diff --git a/custom_components/sonoff/core/devices.py b/custom_components/sonoff/core/devices.py index 61c01813..40ac1104 100644 --- a/custom_components/sonoff/core/devices.py +++ b/custom_components/sonoff/core/devices.py @@ -35,6 +35,7 @@ XLightGroup, XLightL1, XLightL3, + XT5Light, ) from ..number import XPulseWidth from ..remote import XRemote @@ -51,6 +52,7 @@ XEnergySensorDualR3, XEnergySensorPOWR3, XEnergyTotal, + XT5Action, ) from ..switch import ( XSwitch, @@ -289,7 +291,13 @@ def spec(cls, base: str = None, enabled: bool = None, **kwargs) -> type: 136: [spec(XLightB05B, min_ct=0, max_ct=100), RSSI], # Sonoff B05-BL 137: [XLightL1, RSSI], # https://github.com/AlexxIT/SonoffLAN/issues/623#issuecomment-1365841454 - 138: [Switch1, LED, RSSI, XDetach], # MINIR3, MINIR4 + 138: [ + Switch1, + LED, + RSSI, + XDetach, + spec(XRemoteButton, param="action"), + ], # MINIR3, MINIR4 # https://github.com/AlexxIT/SonoffLAN/issues/808 154: [XWiFiDoor, Battery, RSSI], # DW2-Wi-Fi-L 162: SPEC_3CH, # https://github.com/AlexxIT/SonoffLAN/issues/659 @@ -335,6 +343,10 @@ def spec(cls, base: str = None, enabled: bool = None, **kwargs) -> type: ], # Sonoff POWR3 # https://github.com/AlexxIT/SonoffLAN/issues/984 195: [XTemperatureTH], # NSPanel Pro + # https://github.com/AlexxIT/SonoffLAN/issues/1183 + 209: [Switch1, XT5Light, XT5Action], # T5-1C-86 + 210: [Switch1, Switch2, XT5Light, XT5Action], # T5-2C-86 + 211: [Switch1, Switch2, Switch3, XT5Light, XT5Action], # T5-3C-86 1000: [XRemoteButton, Battery], # zigbee_ON_OFF_SWITCH_1000 1256: [spec(XSwitch, base="light")], # ZCL_HA_DEVICEID_ON_OFF_LIGHT 1257: [spec(XLightD1, base="light")], # ZigbeeWhiteLight diff --git a/custom_components/sonoff/core/ewelink/cloud.py b/custom_components/sonoff/core/ewelink/cloud.py index ff01dbf3..35c47e1e 100644 --- a/custom_components/sonoff/core/ewelink/cloud.py +++ b/custom_components/sonoff/core/ewelink/cloud.py @@ -214,9 +214,9 @@ async def send( log += f"{params} | " # protect cloud from DDoS (it can break connection) - while time.time() - self.last_ts < 0.1: + while (delay := self.last_ts + 0.1 - time.time()) > 0: log += "DDoS | " - await asyncio.sleep(0.1) + await asyncio.sleep(delay) self.last_ts = time.time() if sequence is None: diff --git a/custom_components/sonoff/core/ewelink/local.py b/custom_components/sonoff/core/ewelink/local.py index 8a7067d5..82060fc3 100644 --- a/custom_components/sonoff/core/ewelink/local.py +++ b/custom_components/sonoff/core/ewelink/local.py @@ -249,4 +249,6 @@ def decrypt_msg(msg: dict, devicekey: str = None) -> dict: # Fix Sonoff RF Bridge sintax bug if data and data.startswith(b'{"rf'): data = data.replace(b'"="', b'":"') + # fix https://github.com/AlexxIT/SonoffLAN/issues/1160 + data = data.rstrip(b"\x02") return json.loads(data) diff --git a/custom_components/sonoff/light.py b/custom_components/sonoff/light.py index 38860f08..3e87849f 100644 --- a/custom_components/sonoff/light.py +++ b/custom_components/sonoff/light.py @@ -1,9 +1,12 @@ +import time + from homeassistant.components.light import ( COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_ONOFF, COLOR_MODE_RGB, SUPPORT_EFFECT, + SUPPORT_TRANSITION, LightEntity, ) from homeassistant.util import color @@ -45,6 +48,7 @@ class XLight(XEntity, LightEntity): # support on/off and brightness _attr_color_mode = COLOR_MODE_BRIGHTNESS _attr_supported_color_modes = {COLOR_MODE_BRIGHTNESS} + _attr_supported_features = SUPPORT_TRANSITION def set_state(self, params: dict): if self.param in params: @@ -61,17 +65,22 @@ async def async_turn_on( xy_color=None, hs_color=None, effect: str = None, - **kwargs + transition: float = None, + **kwargs, ) -> None: - if brightness == 0: - await self.async_turn_off() - return - if xy_color: rgb_color = color.color_xy_to_RGB(*xy_color) elif hs_color: rgb_color = color.color_hs_to_RGB(*hs_color) + if transition: + await self.transiton(brightness, color_temp, rgb_color, transition) + return + + if brightness == 0: + await self.async_turn_off() + return + if brightness or color_temp or rgb_color or effect: params = self.get_params(brightness, color_temp, rgb_color, effect) else: @@ -83,8 +92,13 @@ async def async_turn_on( await self.ewelink.send( self.device, {self.param: "on"}, query_cloud=False ) + await self.ewelink.send( - self.device, params, {"cmd": "dimmable", **params}, cmd_lan="dimmable" + self.device, + params, + {"cmd": "dimmable", **params}, + cmd_lan="dimmable", + query_cloud=kwargs.get("query_cloud", True), ) else: await self.ewelink.send(self.device, {self.param: "on"}) @@ -92,6 +106,36 @@ async def async_turn_on( async def async_turn_off(self, **kwargs) -> None: await self.ewelink.send(self.device, {self.param: "off"}) + async def transiton( + self, + brightness: int, + color_temp: int, + rgb_color, + transition: float, + ): + br0 = self.brightness or 0 + br1 = brightness + ct0 = self.color_temp or self.min_mireds + ct1 = color_temp + rgb0 = self.rgb_color or [0, 0, 0] + rgb1 = rgb_color + + t0 = time.time() + + while (k := (time.time() - t0) / transition) < 1: + if br1 is not None: + brightness = br0 + round((br1 - br0) * k) + if ct1 is not None: + color_temp = ct0 + round((ct1 - ct0) * k) + if rgb1 is not None: + rgb_color = [rgb0[i] + round((rgb1[i] - rgb0[i]) * k) for i in range(3)] + + await self.async_turn_on( + brightness, color_temp, rgb_color, query_cloud=False + ) + + await self.async_turn_on(br1, ct1, rgb1) + # noinspection PyAbstractClass, UIID36 class XDimmer(XLight): @@ -199,7 +243,7 @@ class XLightB1(XLight): _attr_effect_list = list(UIID22_MODES.keys()) # support on/off, brightness, color_temp and RGB _attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_RGB} - _attr_supported_features = SUPPORT_EFFECT + _attr_supported_features = SUPPORT_EFFECT | SUPPORT_TRANSITION def set_state(self, params: dict): XLight.set_state(self, params) @@ -295,7 +339,7 @@ class XLightL1(XLight): # support on/off, brightness, RGB _attr_supported_color_modes = {COLOR_MODE_RGB} - _attr_supported_features = SUPPORT_EFFECT + _attr_supported_features = SUPPORT_EFFECT | SUPPORT_TRANSITION def set_state(self, params: dict): XLight.set_state(self, params) @@ -314,23 +358,23 @@ def set_state(self, params: dict): ) def get_params(self, brightness, color_temp, rgb_color, effect) -> dict: + params = {} if effect: - return self.modes.get(effect) - if brightness or rgb_color: - # support bright and color in one command - params = {"mode": 1} - if brightness: - params["bright"] = conv(brightness, 1, 255, 1, 100) - if rgb_color: - params.update( - { - "colorR": rgb_color[0], - "colorG": rgb_color[1], - "colorB": rgb_color[2], - "light_type": 1, - } - ) - return params + params.update(self.modes[effect]) + if brightness: + params.setdefault("mode", 1) + params["bright"] = conv(brightness, 1, 255, 1, 100) + if rgb_color: + params.setdefault("mode", 1) + params.update( + { + "colorR": rgb_color[0], + "colorG": rgb_color[1], + "colorB": rgb_color[2], + "light_type": 1, + } + ) + return params # noinspection PyAbstractClass @@ -729,7 +773,7 @@ class XLightB02(XLight): _attr_effect_list = list(B02_MODE_PAYLOADS.keys()) # support on/off, brightness and color_temp _attr_supported_color_modes = {COLOR_MODE_COLOR_TEMP} - _attr_supported_features = SUPPORT_EFFECT + _attr_supported_features = SUPPORT_EFFECT | SUPPORT_TRANSITION # ewelink specs min_br = 1 @@ -1027,3 +1071,33 @@ async def async_turn_on( async def async_turn_off(self, **kwargs) -> None: await self.ewelink.send(self.device, {"lightswitch": 0}) + + +class XT5Light(XEntity, LightEntity): + params = {"lightSwitch", "lightMode"} + + _attr_effect_list = ["0", "1", "2", "3", "4", "5", "6", "7"] + _attr_supported_features = SUPPORT_EFFECT + + def set_state(self, params: dict): + if "lightSwitch" in params: + self._attr_is_on = params["lightSwitch"] == "on" + + if "lightMode" in params: + self._attr_effect = str(params["lightMode"]) + + async def async_turn_on( + self, brightness: int = None, effect: str = None, **kwargs + ) -> None: + params = {} + + if effect and effect != "0": + params["lightMode"] = int(effect) + + if not params: + params["lightSwitch"] = "on" + + await self.ewelink.send(self.device, params) + + async def async_turn_off(self, **kwargs) -> None: + await self.ewelink.send(self.device, {"lightSwitch": "off"}) diff --git a/custom_components/sonoff/manifest.json b/custom_components/sonoff/manifest.json index 693e79b8..30a4c02f 100644 --- a/custom_components/sonoff/manifest.json +++ b/custom_components/sonoff/manifest.json @@ -1,19 +1,19 @@ { "domain": "sonoff", "name": "Sonoff", - "config_flow": true, - "documentation": "https://github.com/AlexxIT/SonoffLAN", - "issue_tracker": "https://github.com/AlexxIT/SonoffLAN/issues", "codeowners": [ "@AlexxIT" ], + "config_flow": true, "dependencies": [ "http", "zeroconf" ], + "documentation": "https://github.com/AlexxIT/SonoffLAN", + "iot_class": "local_push", + "issue_tracker": "https://github.com/AlexxIT/SonoffLAN/issues", "requirements": [ "pycryptodome>=3.6.6" ], - "version": "3.5.2", - "iot_class": "local_push" -} + "version": "3.5.3" +} \ No newline at end of file diff --git a/custom_components/sonoff/sensor.py b/custom_components/sonoff/sensor.py index 05b7092c..0843180c 100644 --- a/custom_components/sonoff/sensor.py +++ b/custom_components/sonoff/sensor.py @@ -305,10 +305,11 @@ def internal_available(self) -> bool: class XRemoteButton(XEntity, SensorEntity): + _attr_native_value = "" + def __init__(self, ewelink: XRegistry, device: dict): XEntity.__init__(self, ewelink, device) self.params = {"key"} - self._attr_native_value = "" def set_state(self, params: dict): button = params.get("outlet") @@ -324,6 +325,29 @@ async def clear_state(self): self._async_write_ha_state() +class XT5Action(XEntity, SensorEntity): + uid = "action" + _attr_native_value = "" + + def __init__(self, ewelink: XRegistry, device: dict): + XEntity.__init__(self, ewelink, device) + self.params = {"triggerType", "slide"} + + def set_state(self, params: dict): + if params.get("triggerType") == 2: + self._attr_native_value = "touch" + asyncio.create_task(self.clear_state()) + + if slide := params.get("slide"): + self._attr_native_value = f"slide_{slide}" + asyncio.create_task(self.clear_state()) + + async def clear_state(self): + await asyncio.sleep(0.5) + self._attr_native_value = "" + self._async_write_ha_state() + + class XUnknown(XEntity, SensorEntity): _attr_device_class = SensorDeviceClass.TIMESTAMP diff --git a/tests/__init__.py b/tests/__init__.py index 89334f7b..e2dffa26 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -46,7 +46,7 @@ def init(device: dict, config: dict = None) -> (XRegistry, List[XEntity]): reg.dispatcher_connect(SIGNAL_ADD_ENTITIES, lambda x: entities.extend(x)) entities += reg.setup_devices(devices) - hass = HomeAssistant() + hass = HomeAssistant("") for entity in entities: if not isinstance(entity, Entity): continue diff --git a/tests/test_entity.py b/tests/test_entity.py index 98742256..fee5eec8 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -34,6 +34,7 @@ XLightL1, XLightL3, XLightB05B, + XT5Light, ) from custom_components.sonoff.number import XNumber, XPulseWidth from custom_components.sonoff.sensor import ( @@ -43,6 +44,7 @@ XTemperatureNS, XUnknown, XEnergySensorDualR3, + XT5Action, ) from custom_components.sonoff.switch import ( XSwitch, @@ -1586,6 +1588,8 @@ def test_minir4(): ], "addSubDevState": "off", "addTimeOut": 10, + + "key": 0, # added manually }, "model": "MINIR4", } @@ -1596,3 +1600,47 @@ def test_minir4(): switch: SwitchEntity = next(e for e in entities if e.uid == "detach") assert switch.state == "on" + + action: XRemoteButton = next(e for e in entities if e.uid == "action") + assert action.state == "" + + action.internal_update({"key": 0}) + assert action.state == "single" + + +def test_t5(): + entities = get_entitites( + { + "extra": {"uiid": 211}, + "params": { + "switches": [ + {"outlet": 0, "switch": "on"}, + {"outlet": 1, "switch": "off"}, + {"outlet": 2, "switch": "off"}, + ], + "lightSwitch": "off", + "lightMode": 4, + "slide": 2, + }, + "model": "T5-3C-86", + } + ) + + light: XT5Light = entities[3] + assert light.state == "off" + assert light.effect == "4" + + light.internal_update({"lightSwitch": "on"}) + assert light.state == "on" + + light.internal_update({"lightMode": 1}) + assert light.effect == "1" + + action: XT5Action = entities[4] + assert action.state == "" + + action.internal_update({"triggerType": 2}) + assert action.state == "touch" + + action.internal_update({"slide": 2}) + assert action.state == "slide_2" diff --git a/tests/test_misc.py b/tests/test_misc.py index b2a97f6b..adcf0293 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1,6 +1,6 @@ import asyncio -from custom_components.sonoff.core.ewelink import XDevice, XRegistry +from custom_components.sonoff.core.ewelink import XDevice, XRegistry, XRegistryLocal from . import save_to @@ -36,3 +36,14 @@ def test_bulk(): assert registry_send[0][1] == { "switches": [{"outlet": 1, "switch": "on"}, {"outlet": 2, "switch": "off"}] } + + +def test_issue_1160(): + payload = XRegistryLocal.decrypt_msg( + { + "iv": "MTA4MDc1MTQ5NzE5ODE2Ng==", + "data": "D85ho6GLI5uFX2b1+vohUIb+Xt99f55wxsBsNhqpPQdQ/WNc3ZTlCi1UVFiFU5cnaCPjvXPG6pqfHqXdtCO2fA==", + }, + "9b0810bc-557a-406c-8266-614767890531", + ) + assert payload == {"switches": [{"outlet": 0, "switch": "off"}]}