Skip to content

Commit

Permalink
59 support for wyze light strips (#98)
Browse files Browse the repository at this point in the history
  • Loading branch information
shauntarves authored Oct 26, 2022
1 parent bd5614b commit a043d6f
Show file tree
Hide file tree
Showing 6 changed files with 507 additions and 35 deletions.
94 changes: 83 additions & 11 deletions wyze_sdk/api/devices/bulbs.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
from datetime import timedelta
from typing import Optional, Sequence
from typing import Optional, Sequence, Tuple, Union

from wyze_sdk.api.base import BaseClient
from wyze_sdk.errors import WyzeFeatureNotSupportedError
from wyze_sdk.errors import WyzeFeatureNotSupportedError, WyzeRequestError
from wyze_sdk.models.devices import (Bulb, BulbProps, DeviceModels, DeviceProp,
DeviceProps, MeshBulb, PropDef)
from wyze_sdk.models.devices.bulbs import BaseBulb
from wyze_sdk.models.devices.lights import LightProps
from wyze_sdk.models.devices.lights import LightControlMode, LightProps, LightVisualEffect
from wyze_sdk.service import WyzeResponse, api_service


class BulbsClient(BaseClient):
"""A Client that services Wyze bulbs/lights.
"""

LIGHT_STRIP_PRO_SUBSECTION_COUNT = 16

def _list_bulbs(self) -> Sequence[dict]:
return [device for device in super()._list_devices(
) if device['product_model'] in DeviceModels.BULB]
Expand Down Expand Up @@ -149,13 +151,18 @@ def set_color_temp(self, *, device_mac: str, device_model: str, color_temp: int,
"""

if device_model in DeviceModels.MESH_BULB:
prop_def = BulbProps.color_temp_mesh()
prop_def.validate(color_temp)
_prop_def = BulbProps.color_temp_mesh()
_prop_def.validate(color_temp)

_prop = DeviceProp(definition=PropDef(_prop_def.pid, str), value=str(color_temp))
if device_model in DeviceModels.LIGHT_STRIP:
_prop = [_prop]
_prop.append(DeviceProp(definition=LightProps.control_light(), value=LightControlMode.TEMPERATURE.code))

return super()._api_client().run_action_list(
actions={
"key": "set_mesh_property",
"prop": DeviceProp(definition=PropDef(prop_def.pid, str), value=str(color_temp)),
"prop": _prop,
"device_mac": device_mac,
"provider_key": device_model,
}
Expand All @@ -167,28 +174,66 @@ def set_color_temp(self, *, device_mac: str, device_model: str, color_temp: int,
return super()._api_client().set_device_property_list(
mac=device_mac, model=device_model, props=DeviceProp(definition=PropDef(prop_def.pid, str), value=str(color_temp)))

def set_color(self, *, device_mac: str, device_model: str, color: str, **kwargs) -> WyzeResponse:
def set_color(self, *, device_mac: str, device_model: str, color: Union[str, Sequence[str]], **kwargs) -> WyzeResponse:
"""Sets the color of a bulb.
For Light Strip Pro devices, this color can be a list of 16 colors that will
be used to set the value for each subsection of the light strip. The list is
ordered like:
``
15 14 13 12
8 9 10 11
7 6 5 4
0 1 2 3
``
Args:
:param str device_mac: The device mac. e.g. ``ABCDEF1234567890``
:param str device_model: The device model. e.g. ``WLPA19``
:param str color: The new color temperature. e.g. ``ff0000``
:param color: The new color(s). e.g. ``ff0000`` or ``['ff0000', '00ff00', ...]``
:type color: Union[str, Sequence[str]]
:rtype: WyzeResponse
:raises WyzeFeatureNotSupportedError: If the bulb doesn't support color
or color is a list and the bulb doesn't support color sections
"""
if device_model not in DeviceModels.MESH_BULB:
raise WyzeFeatureNotSupportedError("set_color")

prop_def = BulbProps.color()
prop_def.validate(color)
_color_prop_def = BulbProps.color()
_color_prop = None

if isinstance(color, (list, Tuple)):
if device_model not in DeviceModels.LIGHT_STRIP_PRO:
raise WyzeFeatureNotSupportedError("The target device type does not support color sections.")
if len(color) != self.LIGHT_STRIP_PRO_SUBSECTION_COUNT:
raise WyzeRequestError(f"Color must specify values for all {self.LIGHT_STRIP_PRO_SUBSECTION_COUNT} subsections.")

color = list(map(lambda _color: _color.upper(), color))
for _color in color:
_color_prop_def.validate(_color)
else:
color = color.upper()
_color_prop_def.validate(color)
_color_prop = DeviceProp(definition=PropDef(_color_prop_def.pid, str), value=str(color))

_prop = _color_prop

# if we're dealing with a light strip, we need to set the color control mode
if device_model in DeviceModels.LIGHT_STRIP:
_prop = [DeviceProp(definition=LightProps.control_light(), value=LightControlMode.COLOR.code)]
# Pro light strips also need their subsection property updated
if device_model in DeviceModels.LIGHT_STRIP_PRO:
if isinstance(color, str):
# turn this into a list of subsections for joining
color = [color] * self.LIGHT_STRIP_PRO_SUBSECTION_COUNT
_prop.append(DeviceProp(definition=LightProps.subsection(), value="00" + "#00".join(color)))

return super()._api_client().run_action_list(
actions={
"key": "set_mesh_property",
"prop": DeviceProp(definition=prop_def, value=color),
"prop": _prop,
"device_mac": device_mac,
"provider_key": device_model,
}
Expand Down Expand Up @@ -258,3 +303,30 @@ def _set_away_mode_disbled(self, *, device_mac: str, device_model: str, **kwargs
)
return super()._api_client().set_device_property(
mac=device_mac, model=device_model, pid=prop_def.pid, value="0")

def set_effect(self, *, device_mac: str, device_model: str, effect: LightVisualEffect, **kwargs) -> WyzeResponse:
"""Sets the visual/sound effect for a light.
Args:
:param str device_mac: The device mac. e.g. ``ABCDEF1234567890``
:param str device_model: The device model. e.g. ``WLPA19``
:param str LightVisualEffect: The new visual effect definition.
:rtype: WyzeResponse
:raises WyzeFeatureNotSupportedError: If the light doesn't support effects
"""
if device_model not in DeviceModels.LIGHT_STRIP:
raise WyzeFeatureNotSupportedError("set_effect")

_prop = effect.to_plist()
_prop.append(DeviceProp(definition=LightProps.control_light(), value=LightControlMode.FRAGMENTED.code))

return super()._api_client().run_action_list(
actions={
"key": "set_mesh_property",
"prop": _prop,
"device_mac": device_mac,
"provider_key": device_model,
}
)
2 changes: 1 addition & 1 deletion wyze_sdk/models/devices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
DeviceProp, DeviceProps, LockableMixin, MotionMixin,
PropDef, SwitchableMixin, VoltageMixin)
from .lights import LightProps, Light # noqa
from .bulbs import BulbProps, Bulb, MeshBulb, WhiteBulb # noqa
from .bulbs import BulbProps, BaseBulb, Bulb, MeshBulb, WhiteBulb # noqa
from .cameras import BaseStation, Camera, CameraProps # noqa
from .locks import (Lock, LockEventType, LockGateway, LockProps, # noqa
LockRecord, LockRecordDetail)
Expand Down
21 changes: 18 additions & 3 deletions wyze_sdk/models/devices/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import distutils.util
import json
import logging
from abc import ABCMeta
from datetime import datetime
Expand Down Expand Up @@ -39,7 +40,9 @@ class DeviceModels(object):

BULB_WHITE = ['WLPA19']
BULB_WHITE_V2 = ['HL_HWB2']
MESH_BULB = ['WLPA19C']
LIGHT_STRIP_PRO = ['HL_LSLP']
LIGHT_STRIP = ['HL_LSL'] + LIGHT_STRIP_PRO
MESH_BULB = ['WLPA19C'] + LIGHT_STRIP

BULB = BULB_WHITE + BULB_WHITE_V2 + MESH_BULB

Expand Down Expand Up @@ -174,10 +177,10 @@ def api_value(self) -> Any:
if self.definition.type == bool:
if self.definition.api_type == int:
self.logger.debug(f"returning boolean value {self.value} as int")
return int(self.value)
return int(self.value if self.value else False)
elif self.definition.api_type == str:
self.logger.debug(f"returning boolean value {self.value} as str")
return str(int(self.value))
return str(int(self.value if self.value else False))
if isinstance(self.value, self.definition.api_type):
self.logger.debug(f"value {self.value} is already of configured api type {self.definition.api_type}, returning unchanged")
return self.value
Expand All @@ -187,6 +190,18 @@ def api_value(self) -> Any:
except ValueError:
self.logger.warning(f"could not cast value `{self.value}` into expected api type {self.definition.api_type}")

def __str__(self):
return f"Property {self.definition.pid}: {self.value} [API value: {self.api_value}]"

def to_json(self):
to_return = {
'pid': self.definition.pid,
'pvalue': json.dumps(self.api_value),
}
if self.ts is not None:
to_return['ts'] = str(int(self.ts.replace(microsecond=0).timestamp()))
return to_return


class DeviceProps(object):

Expand Down
Loading

0 comments on commit a043d6f

Please sign in to comment.