From acb6f60f9143d6869bb79779c59f2fe6ba094bf3 Mon Sep 17 00:00:00 2001 From: Jeff Steinbok Date: Sat, 31 Aug 2024 20:51:37 -0700 Subject: [PATCH 1/4] Fix preset modes --- custom_components/dreo/pydreo/constant.py | 17 +- custom_components/dreo/pydreo/pydreofan.py | 12 +- .../get_device_state_HCF001S_1.json | 117 ++++++++ .../api_responses/get_devices_HCF001S.json | 254 ++++++++++++++++++ tests/pydreo/test_pydreofan.py | 29 ++ 5 files changed, 420 insertions(+), 9 deletions(-) create mode 100644 tests/pydreo/api_responses/get_device_state_HCF001S_1.json create mode 100644 tests/pydreo/api_responses/get_devices_HCF001S.json diff --git a/custom_components/dreo/pydreo/constant.py b/custom_components/dreo/pydreo/constant.py index e25e99f..49ec15b 100755 --- a/custom_components/dreo/pydreo/constant.py +++ b/custom_components/dreo/pydreo/constant.py @@ -78,11 +78,6 @@ DREO_API_REGION_US = "us" DREO_API_REGION_EU = "eu" -FAN_MODE_NORMAL = "normal" -FAN_MODE_NATURAL = "natural" -FAN_MODE_AUTO = "auto" -FAN_MODE_SLEEP = "sleep" -FAN_MODE_TURBO = "turbo" HEATER_MODE_COOLAIR = "coolair" HEATER_MODE_HOTAIR = "hotair" HEATER_MODE_ECO = "eco" @@ -217,3 +212,15 @@ class HVACMode(StrEnum): # Only the fan is on, not fan and another mode like cool FAN_ONLY = "fan_only" + +FAN_MODE_STRINGS = { + "device_fans_mode_straight": "normal", + "device_fans_mode_natural": "natural", + "device_control_mode_sleep": "sleep", + "device_fans_mode_sleep": "sleep", + "device_fans_mode_auto": "auto", + "device_control_mode_auto": "auto", + "device_control_mode_turbo": "turbo", + "base_reverse": "reverse", + "device_control_custom": "custom" +} diff --git a/custom_components/dreo/pydreo/pydreofan.py b/custom_components/dreo/pydreo/pydreofan.py index e7103c9..6667b0b 100644 --- a/custom_components/dreo/pydreo/pydreofan.py +++ b/custom_components/dreo/pydreo/pydreofan.py @@ -26,7 +26,8 @@ FIXEDCONF_KEY, OscillationMode, TemperatureUnit, - SPEED_RANGE + SPEED_RANGE, + FAN_MODE_STRINGS ) from .pydreobasedevice import PyDreoBaseDevice @@ -123,9 +124,11 @@ def parse_preset_modes(self, details: Dict[str, list]) -> tuple[str, int]: control = controls_conf.get("control", None) if (control is not None): for control_item in control: - if control_item.get("type", None) == "Mode": + if (control_item.get("type", None) == "Mode" or + control_item.get("type", None) == "CFFan"): for mode_item in control_item.get("items", None): - text = mode_item.get("image", None).split("_")[1] + text_id = mode_item.get("text", None) + text = FAN_MODE_STRINGS[text_id] value = mode_item.get("value", None) preset_modes.append((text, value)) schedule = controls_conf.get("schedule", None) @@ -133,7 +136,8 @@ def parse_preset_modes(self, details: Dict[str, list]) -> tuple[str, int]: modes = schedule.get("modes", None) if (modes is not None): for mode_item in modes: - text = mode_item.get("icon", None).split("_")[1] + text_id = mode_item.get("title", None) + text = FAN_MODE_STRINGS[text_id] value = mode_item.get("value", None) if (text, value) not in preset_modes: preset_modes.append((text, value)) diff --git a/tests/pydreo/api_responses/get_device_state_HCF001S_1.json b/tests/pydreo/api_responses/get_device_state_HCF001S_1.json new file mode 100644 index 0000000..947894e --- /dev/null +++ b/tests/pydreo/api_responses/get_device_state_HCF001S_1.json @@ -0,0 +1,117 @@ +{ + "code": 0, + "msg": "OK", + "data": { + "mixed": { + "wifi_rssi": { + "state": -31, + "timestamp": 1724564509 + }, + "scheid": { + "state": 0, + "timestamp": 1724564509 + }, + "timeron": { + "state": { + "du": 0, + "ts": 1719426564 + }, + "timestamp": null + }, + "scheon": { + "state": false, + "timestamp": 1724564509 + }, + "mode": { + "state": 1, + "timestamp": 1724564509 + }, + "mcuon": { + "state": true, + "timestamp": 1724564509 + }, + "network_latency": { + "state": 131, + "timestamp": 1724564509 + }, + "module_hardware_model": { + "state": "HeFi", + "timestamp": 1724564509 + }, + "mcu_firmware_version": { + "state": "2.1.8", + "timestamp": 1724564509 + }, + "colortemp": { + "state": 25, + "timestamp": 1724564509 + }, + "module_hardware_mac": { + "state": "001cc27df25e", + "timestamp": 1724564509 + }, + "muteon": { + "state": true, + "timestamp": 1724564509 + }, + "lighton": { + "state": false, + "timestamp": 1724882948 + }, + "wifi_ssid": { + "state": "connected_1", + "timestamp": 1721672047 + }, + "mcu_hardware_model": { + "state": "FP6063U/US", + "timestamp": 1724564509 + }, + "windlevel": { + "state": 5, + "timestamp": 1724893596 + }, + "wrong": { + "state": 0, + "timestamp": 1724564509 + }, + "module_firmware_version": { + "state": "3.2.3", + "timestamp": 1724564509 + }, + "connected": { + "state": true, + "timestamp": 1724564509 + }, + "timeroff": { + "state": { + "du": 0, + "ts": 1721672046 + }, + "timestamp": null + }, + "predefine": { + "state": "0", + "timestamp": 1724564509 + }, + "_ota": { + "state": 0, + "timestamp": 1724564509 + }, + "brightness": { + "state": 64, + "timestamp": 1724564509 + }, + "scenes": { + "state": "{\"mode\":0,\"du\":600,\"minbri\":0,\"maxbri\":0}", + "timestamp": 1724564509 + }, + "fanon": { + "state": false, + "timestamp": 1724923407 + } + }, + "sn": "1706512872524279810-150f28ec46b7b969:001:0000000000w", + "productId": "1706512872524279810", + "region": "us-east-2/emq" + } +} \ No newline at end of file diff --git a/tests/pydreo/api_responses/get_devices_HCF001S.json b/tests/pydreo/api_responses/get_devices_HCF001S.json new file mode 100644 index 0000000..71a8cb5 --- /dev/null +++ b/tests/pydreo/api_responses/get_devices_HCF001S.json @@ -0,0 +1,254 @@ +{ + "code": 0, + "msg": "OK", + "data": { + "currentPage": 1, + "pageSize": 100, + "totalNum": 2, + "totalPage": 1, + "familyRooms": null, + "list": [ + { + "deviceId": "1769274833431957506", + "sn": "HCF001S_1", + "brand": "Dreo", + "model": "DR-HCF001S", + "productId": "1706512872524279810", + "productName": "Ceiling Fan", + "deviceName": "Master Bedroom Ceiling Fan", + "shared": false, + "series": null, + "seriesName": "CLF521S", + "type": 0, + "owner": true, + "familyId": null, + "familyName": null, + "roomId": null, + "roomName": null, + "roomNameI18Key": "", + "color": "w", + "controlsConf": { + "template": "DR-HCF001S", + "lottie": { + "key": "poweron", + "frames": [ + { + "value": 0, + "frame": [ + 0 + ] + }, + { + "value": 1, + "frame": [ + 2 + ] + } + ] + }, + "schedule": { + "modes": [] + }, + "cards": [ + { + "type": 6, + "title": "home_bedtime_light", + "icon": "ic_bedtime_light", + "image": "", + "url": "dreo://nav/device/bedtimelight?deviceSn=${sn}", + "show": true, + "key": "" + }, + { + "type": 8, + "title": "", + "icon": "", + "image": "", + "url": "dreo://nav/device/schedule?deviceSn={sn}", + "show": true, + "key": "" + }, + { + "type": 6, + "title": "device_settings_title", + "icon": "ic_setting", + "image": "", + "url": "dreo://nav/device/setting?deviceSn=${sn}", + "show": true, + "key": "setting" + } + ], + "feature": { + "schedule": { + "enable": "", + "localSupport": true, + "module": [ + { + "type": "HeFi", + "version": "0.0.1" + } + ] + } + }, + "preference": [ + { + "id": "200", + "type": "Panel Sound", + "title": "dev_ctrl_audio_feedback", + "image": "ic_mute", + "reverse": true, + "cmd": "muteon" + } + ], + "control": [ + { + "id": "110", + "type": "Speed", + "title": "device_control_speed", + "items": [ + { + "text": "1", + "cmd": "windlevel", + "value": 1 + }, + { + "text": "12", + "cmd": "windlevel", + "value": 12 + } + ] + }, + { + "id": "130", + "type": "CFFan", + "title": "base_fan", + "cmd": "fanon", + "items": [ + { + "text": "base_reverse", + "textColors": [ + "#D5D6D7", + "#1D1D1D" + ], + "image": "ic_cf_mode_reverse", + "imageColors": [ + "#D5D6D7", + "#FFBB33" + ], + "cmd": "mode", + "value": 4, + "toast": "dev_ctrl_reverse_toast" + }, + { + "text": "device_fans_mode_straight", + "textColors": [ + "#D5D6D7", + "#1D1D1D" + ], + "image": "ic_normal_wind", + "imageColors": [ + "#D5D6D7", + "#25D7E4" + ], + "cmd": "mode", + "value": 1 + }, + { + "text": "device_fans_mode_natural", + "textColors": [ + "#D5D6D7", + "#1D1D1D" + ], + "image": "ic_natural_wind", + "imageColors": [ + "#D5D6D7", + "#2CDD96" + ], + "cmd": "mode", + "value": 2 + }, + { + "text": "device_control_mode_sleep", + "textColors": [ + "#D5D6D7", + "#1D1D1D" + ], + "image": "ic_sleep_wind", + "imageColors": [ + "#D5D6D7", + "#6249DF" + ], + "cmd": "mode", + "value": 3 + } + ] + }, + { + "id": "140", + "type": "CFLight", + "title": "device_control_light", + "cmd": "lighton", + "items": [ + { + "type": "light", + "text": "device_fans_mode_natural", + "image": "ic_cf_light", + "cmd": "brightness", + "maxValue": 100, + "minValue": 1 + }, + { + "type": "color", + "text": "device_control_mode_sleep", + "image": "ic_color_bar", + "cmd": "colortemp", + "maxValue": 100, + "minValue": 0 + } + ] + } + ], + "category": "Ceiling Fan", + "version": { + "minControlVer": "2.6.2", + "minPairingVer": "2.6.2" + }, + "setting": [ + { + "text": "Firmware Version", + "image": "image.png", + "value": "1.0.0", + "url": "dreo://control" + } + ] + }, + "mainConf": { + "isSmart": true, + "isWifi": true, + "isBluetooth": true, + "isVoiceControl": true + }, + "resourcesConf": { + "imageSmallSrc": "https://resources.dreo-cloud.com/app/202403/5/2b990958eba64c5baf91e546e007f0dd.png", + "imageFullSrc": "https://resources.dreo-cloud.com/app/202402/6/8d6598eea10149d4a62d95eb3d3d948b.zip", + "imageSmallDarkSrc": "", + "imageFullDarkSrc": "" + }, + "servicesConf": [ + { + "key": "user_manual", + "value": "https://resources.dreo-cloud.com/app/202403/11/5b809a25081d4a2ebf0f7b67504413fc.pdf" + } + ], + "userManuals": [ + { + "url": "https://resources.dreo-cloud.com/app/202403/11/5b809a25081d4a2ebf0f7b67504413fc.pdf", + "icon": null, + "desc": "User Manual", + "lang": "en" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/pydreo/test_pydreofan.py b/tests/pydreo/test_pydreofan.py index 0e544e7..4ae2407 100644 --- a/tests/pydreo/test_pydreofan.py +++ b/tests/pydreo/test_pydreofan.py @@ -108,3 +108,32 @@ def test_circulator_load_and_send_commands(self): with pytest.raises(ValueError): fan.fan_speed = 10 + + + def test_HCF005S(self): # pylint: disable=invalid-name + """Load fan and test sending commands.""" + + self.get_devices_file_name = "get_devices_HCF001S.json" + self.manager.load_devices() + assert len(self.manager.fans) == 1 + fan = self.manager.fans[0] + assert fan.speed_range == (1, 12) + assert fan.preset_modes == ['normal', 'natural', 'sleep', 'reverse'] + + with patch('pydreo.PyDreo.send_command') as mock_send_command: + fan.is_on = True + mock_send_command.assert_called_once_with(fan, {POWERON_KEY: True}) + + with patch('pydreo.PyDreo.send_command') as mock_send_command: + fan.preset_mode = 'normal' + mock_send_command.assert_called_once_with(fan, {MODE_KEY: 1}) + + with pytest.raises(ValueError): + fan.preset_mode = 'not_a_mode' + + with patch('pydreo.PyDreo.send_command') as mock_send_command: + fan.fan_speed = 3 + mock_send_command.assert_called_once_with(fan, {WINDLEVEL_KEY: 3}) + + with pytest.raises(ValueError): + fan.fan_speed = 13 \ No newline at end of file From 416b15b6a951827a62910e2227da1c32f356b587 Mon Sep 17 00:00:00 2001 From: Jeff Steinbok Date: Sat, 31 Aug 2024 20:53:24 -0700 Subject: [PATCH 2/4] Fix typo --- tests/pydreo/test_pydreofan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pydreo/test_pydreofan.py b/tests/pydreo/test_pydreofan.py index 4ae2407..4598abb 100644 --- a/tests/pydreo/test_pydreofan.py +++ b/tests/pydreo/test_pydreofan.py @@ -110,7 +110,7 @@ def test_circulator_load_and_send_commands(self): fan.fan_speed = 10 - def test_HCF005S(self): # pylint: disable=invalid-name + def test_HCF005S(self): # pylint: disable=invalid-name """Load fan and test sending commands.""" self.get_devices_file_name = "get_devices_HCF001S.json" From 19a50cd6b1bb93b5ce7fdc9850d53f978d17bdb9 Mon Sep 17 00:00:00 2001 From: Jeff Steinbok Date: Sat, 31 Aug 2024 21:31:24 -0700 Subject: [PATCH 3/4] Add FanEntityFeature.TURN_ON | TURN_OFF --- custom_components/dreo/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/dreo/fan.py b/custom_components/dreo/fan.py index dbd4835..47af184 100644 --- a/custom_components/dreo/fan.py +++ b/custom_components/dreo/fan.py @@ -89,7 +89,7 @@ def extra_state_attributes(self) -> dict[str, Any]: @property def supported_features(self) -> int: """Return the list of supported features.""" - supported_features = FanEntityFeature.SET_SPEED + supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_ON | FanEntityFeature.TURN_OFF if (self.device.preset_mode is not None): supported_features = supported_features | FanEntityFeature.PRESET_MODE if (self.device.oscillating is not None): From b1b1fbd33c3a2f3bec5589df16b7f8eca5e1bdaf Mon Sep 17 00:00:00 2001 From: Jeff Steinbok Date: Sat, 31 Aug 2024 22:19:07 -0700 Subject: [PATCH 4/4] Upgrade to Python 3.12 --- .github/workflows/pytest.yaml | 4 ++-- requirements.test.txt | 4 ++-- tests/pydreo/test_helpers.py | 4 +--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 98e3597..7205449 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -24,10 +24,10 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.11 + - name: Set up Python 3.12 uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/requirements.test.txt b/requirements.test.txt index 0b0d70f..b00fc21 100644 --- a/requirements.test.txt +++ b/requirements.test.txt @@ -1,5 +1,5 @@ # Strictly for tests pytest pytest-cov -pytest-homeassistant-custom-component -pyyaml \ No newline at end of file +pyyaml +homeassistant \ No newline at end of file diff --git a/tests/pydreo/test_helpers.py b/tests/pydreo/test_helpers.py index cd5411f..249ea74 100644 --- a/tests/pydreo/test_helpers.py +++ b/tests/pydreo/test_helpers.py @@ -2,13 +2,11 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from .imports import PyDreo, Helpers + from .imports import Helpers from . import call_json - from .testbase import TestBase else: from imports import * # pylint: disable=W0401,W0614 import call_json - from testbase import TestBase class TestHelpers: """Test Helpers class."""