Skip to content

Commit 8861ec5

Browse files
authored
Merge pull request #149 from webdjoe/esl100mc
Add Valceno device, improve color handling, add Oasis Mist Humidifier, improve tests
2 parents c65c7ef + 30e4384 commit 8861ec5

17 files changed

+1487
-559
lines changed

.pylintrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ disable=
2626
format,
2727
abstract-class-little-used,
2828
abstract-method,
29+
arguments-differ,
2930
cyclic-import,
3031
duplicate-code,
3132
global-statement,
3233
inconsistent-return-statements,
34+
invalid-name,
3335
locally-disabled,
36+
no-self-use,
3437
not-an-iterable,
3538
redefined-variable-type,
3639
too-few-public-methods,

README.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ pip install pyvesync
102102
2. Classic 300S
103103
3. LUH-D301S-WEU Dual (200S)
104104
4. LV600S
105+
5. OasisMist LUS-04515-WUS
105106

106107
## Usage
107108

@@ -350,16 +351,23 @@ Compatible levels for each model:
350351

351352
**Properties**
352353

354+
`VeSyncBulb.color` - Returns a dataclass with HSV and RGB attributes that are named tuples
355+
356+
```
357+
VeSyncBulb.color.rbg = namedtuple('RGB', ['red', 'green', 'blue'])
358+
VeSyncBulb.color.hsv = namedtuple('HSV', ['hue', 'saturation', 'value'])
359+
```
360+
361+
`VeSyncBulb.color_hsv` - Returns a named tuple with HSV values
362+
363+
`VeSyncBulb.color_rgb` - Returns a named tuple with RGB values
364+
353365
`VeSyncBulb.brightness` - Return brightness in percentage (int values from 1 - 100)
354366

355367
`VeSyncBulb.color_temp_pct` - Return white temperature in percentage (int values from 0 - 100)
356368

357369
`VeSyncBulb.color_temp_kelvin` - Return white temperature in Kelvin (int values from 2700-6500)
358370

359-
`VeSyncBulb.color_value_hsv` - Return color value in HSV format (float 0.0-360.0, float 0.0-100.0, int 0-100 )
360-
361-
`VeSyncBulb.color_value_rgb` - Return color value in RGB format (float values up to 255.0, 255.0, 255.0 )
362-
363371
`VeSyncBulb.color_mode` - Return bulb color mode (string values: 'white' , 'hsv' )
364372

365373
`VeSyncBulb.color_hue` - Return color hue (float values from 0.0 - 360.0)
@@ -368,12 +376,29 @@ Compatible levels for each model:
368376

369377
`VeSyncBulb.color_value` - Return color value (int values from 0 - 100)
370378

379+
*The following properties are also still available for backwards compatibility*
380+
381+
`VeSyncBulb.color_value_hsv` - Return color value in HSV format (float 0.0-360.0, float 0.0-100.0, int 0-100 )
382+
383+
`VeSyncBulb.color_value_rgb` - Return color value in RGB format (float values up to 255.0, 255.0, 255.0 )
384+
385+
371386
**Methods**
372387

373388
`VeSyncBulb.set_brightness(brightness)`
374389
- Set bulb brightness (int values from 0 - 100)
375390
- (also used to set Color Value when in color mode)
376391

392+
`VeSyncBulb.set_hsv(hue, saturation, value)`
393+
- Set bulb color in HSV format
394+
- Arguments: hue (numeric) 0 - 360, saturation (numeric) 0-100, value (numeric) 0-100
395+
- Returns bool
396+
397+
`VeSyncBulb.set_rgb(red, green, blue)`
398+
- Set bulb color in RGB format
399+
- Arguments: red (numeric) 0-255, green (numeric) 0-255, blue (numeric) 0-255
400+
- Returns bool
401+
377402
`VeSyncBulb.set_color_mode(color_mode)`
378403
- Set bulb color mode (string values: `white` , `hsv` )
379404
- `color` may be used as an alias to `hsv`

azure-pipelines.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ jobs:
4242
vmImage: 'ubuntu-20.04'
4343
strategy:
4444
matrix:
45-
Python36:
46-
python.version: '3.6'
4745
Python37:
4846
python.version: '3.7'
4947
Python38:
5048
python.version: '3.8'
49+
Python39:
50+
python.version: '3.9'
5151

5252
steps:
5353
- task: UsePythonVersion@0

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
setup(
1212
name='pyvesync',
13-
version='2.0.5',
13+
version='2.1.0',
1414
description='pyvesync is a library to manage Etekcity\
1515
Devices and Levoit Air Purifier',
1616
long_description=long_description,

src/pyvesync/helpers.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
import logging
55
import time
66
import json
7+
import colorsys
8+
from dataclasses import dataclass, field, InitVar
9+
from typing import NamedTuple, Optional, Union
710
import requests
811

12+
913
logger = logging.getLogger(__name__)
1014

1115
API_BASE_URL = 'https://smartapi.vesync.com'
@@ -22,6 +26,8 @@
2226
USER_TYPE = '1'
2327
BYPASS_APP_V = "VeSync 3.0.51"
2428

29+
NUMERIC = Optional[Union[int, float, str]]
30+
2531

2632
class Helpers:
2733
"""VeSync Helper Functions."""
@@ -276,3 +282,128 @@ def bypass_header():
276282
'Content-Type': 'application/json; charset=UTF-8',
277283
'User-Agent': 'okhttp/3.12.1',
278284
}
285+
286+
@staticmethod
287+
def named_tuple_to_str(named_tuple: NamedTuple) -> str:
288+
"""Convert named tuple to string."""
289+
tuple_str = ''
290+
for key, val in named_tuple._asdict().items():
291+
tuple_str += f'{key}: {val}, '
292+
return tuple_str
293+
294+
295+
class HSV(NamedTuple):
296+
"""HSV color space."""
297+
298+
hue: float
299+
saturation: float
300+
value: float
301+
302+
303+
class RGB(NamedTuple):
304+
"""RGB color space."""
305+
306+
red: float
307+
green: float
308+
blue: float
309+
310+
311+
@dataclass
312+
class Color:
313+
"""Dataclass for color values.
314+
315+
For HSV, pass hue as value in degrees 0-360, saturation and value as values
316+
between 0 and 100.
317+
318+
For RGB, pass red, green and blue as values between 0 and 255.
319+
320+
To instantiate pass kw arguments for colors hue, saturation and value or
321+
red, green and blue.
322+
323+
Instance attributes are:
324+
hsv (nameduple) : hue (0-360), saturation (0-100), value (0-100)
325+
326+
rgb (namedtuple) : red (0-255), green (0-255), blue
327+
328+
"""
329+
330+
red: InitVar[NUMERIC] = field(default=None, repr=False, compare=False)
331+
green: InitVar[NUMERIC] = field(default=None, repr=False, compare=False)
332+
blue: InitVar[NUMERIC] = field(default=None, repr=False, compare=False)
333+
hue: InitVar[NUMERIC] = field(default=None, repr=False, compare=False)
334+
saturation: InitVar[NUMERIC] = field(default=None, repr=False,
335+
compare=False)
336+
value: InitVar[NUMERIC] = field(default=None, repr=False, compare=False)
337+
hsv: HSV = field(init=False)
338+
rgb: RGB = field(init=False)
339+
340+
def __post_init__(self, red, green, blue, hue, saturation, value):
341+
"""Check HSV or RGB Values and create named tuples."""
342+
if any(x is not None for x in [hue, saturation, value]):
343+
self.hsv = HSV(*self.valid_hsv(hue, saturation, value))
344+
self.rgb = self.hsv_to_rgb(hue, saturation, value)
345+
elif any(x is not None for x in [red, green, blue]):
346+
self.rgb = RGB(*self.valid_rgb(red, green, blue))
347+
self.hsv = self.rgb_to_hsv(red, green, blue)
348+
else:
349+
logger.error('No color values provided')
350+
351+
@staticmethod
352+
def min_max(value: Union[int, float, str], min_val: float,
353+
max_val: float, default: float) -> float:
354+
"""Check if value is within min and max values."""
355+
try:
356+
val = max(min_val, (min(max_val, round(float(value), 2))))
357+
except (ValueError, TypeError):
358+
val = default
359+
return val
360+
361+
@classmethod
362+
def valid_hsv(cls, h: Union[int, float, str],
363+
s: Union[int, float, str],
364+
v: Union[int, float, str]) -> tuple:
365+
"""Check if HSV values are valid."""
366+
valid_hue = float(cls.min_max(h, 0, 360, 360))
367+
valid_saturation = float(cls.min_max(s, 0, 100, 100))
368+
valid_value = float(cls.min_max(v, 0, 100, 100))
369+
return (
370+
valid_hue,
371+
valid_saturation,
372+
valid_value
373+
)
374+
375+
@classmethod
376+
def valid_rgb(cls, r: float, g: float, b: float) -> list:
377+
"""Check if RGB values are valid."""
378+
rgb = []
379+
for val in (r, g, b):
380+
valid_val = cls.min_max(val, 0, 255, 255)
381+
rgb.append(valid_val)
382+
return rgb
383+
384+
@staticmethod
385+
def hsv_to_rgb(hue, saturation, value) -> RGB:
386+
"""Convert HSV to RGB."""
387+
return RGB(
388+
*tuple(round(i * 255, 0) for i in colorsys.hsv_to_rgb(
389+
hue / 360,
390+
saturation / 100,
391+
value / 100
392+
))
393+
)
394+
395+
@staticmethod
396+
def rgb_to_hsv(red, green, blue) -> HSV:
397+
"""Convert RGB to HSV."""
398+
hsv_tuple = colorsys.rgb_to_hsv(
399+
red / 255,
400+
green / 255,
401+
blue / 255
402+
)
403+
hsv_factors = [360, 100, 100]
404+
405+
return HSV(
406+
float(round(hsv_tuple[0] * hsv_factors[0], 2)),
407+
float(round(hsv_tuple[1] * hsv_factors[1], 2)),
408+
float(round(hsv_tuple[2] * hsv_factors[2], 0)),
409+
)

src/pyvesync/vesync.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,14 +70,9 @@ class VeSync: # pylint: disable=function-redefined
7070

7171
def __init__(self, username, password, time_zone=DEFAULT_TZ, debug=False):
7272
"""Initialize VeSync class with username, password and time zone."""
73-
self.debug = debug
73+
self._debug = debug
7474
if debug:
75-
logger.setLevel(logging.DEBUG)
76-
bulb_mods.logger.setLevel(logging.DEBUG)
77-
switch_mods.logger.setLevel(logging.DEBUG)
78-
outlet_mods.logger.setLevel(logging.DEBUG)
79-
fan_mods.logger.setLevel(logging.DEBUG)
80-
helpermodule.logger.setLevel(logging.DEBUG)
75+
self.debug = debug
8176
self.username = username
8277
self.password = password
8378
self.token = None
@@ -115,6 +110,29 @@ def __init__(self, username, password, time_zone=DEFAULT_TZ, debug=False):
115110
self.time_zone = DEFAULT_TZ
116111
logger.debug('Time zone is not a string')
117112

113+
@property
114+
def debug(self) -> bool:
115+
"""Return debug flag."""
116+
return self._debug
117+
118+
@debug.setter
119+
def debug(self, new_flag: bool) -> None:
120+
"""Set debug flag."""
121+
log_modules = [bulb_mods,
122+
switch_mods,
123+
outlet_mods,
124+
fan_mods,
125+
helpermodule]
126+
if new_flag:
127+
logger.setLevel(logging.DEBUG)
128+
for m in log_modules:
129+
m.logger.setLevel(logging.DEBUG)
130+
elif new_flag is False:
131+
logger.setLevel(logging.WARNING)
132+
for m in log_modules:
133+
m.logger.setLevel(logging.WARNING)
134+
self._debug = new_flag
135+
118136
@property
119137
def energy_update_interval(self) -> int:
120138
"""Return energy update interval."""

0 commit comments

Comments
 (0)