From 899f0e03c106b26032cef89e4561c79af1992c54 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:34:25 +0200 Subject: [PATCH 01/23] Add Tuya test fixtures (#150835) --- tests/components/tuya/__init__.py | 4 + .../tuya/fixtures/cl_g1cp07dsqnbdbbki.json | 102 +++++ .../tuya/fixtures/fsd_9ecs16c53uqskxw6.json | 151 +++++++ .../tuya/fixtures/jtmspro_xqeob8h6.json | 398 ++++++++++++++++++ .../tuya/fixtures/wk_gc1bxoq2hafxpa35.json | 110 +++++ .../tuya/snapshots/test_climate.ambr | 73 ++++ .../components/tuya/snapshots/test_cover.ambr | 51 +++ tests/components/tuya/snapshots/test_fan.ambr | 58 +++ .../components/tuya/snapshots/test_init.ambr | 124 ++++++ .../components/tuya/snapshots/test_light.ambr | 81 ++++ .../tuya/snapshots/test_select.ambr | 57 +++ 11 files changed, 1209 insertions(+) create mode 100644 tests/components/tuya/fixtures/cl_g1cp07dsqnbdbbki.json create mode 100644 tests/components/tuya/fixtures/fsd_9ecs16c53uqskxw6.json create mode 100644 tests/components/tuya/fixtures/jtmspro_xqeob8h6.json create mode 100644 tests/components/tuya/fixtures/wk_gc1bxoq2hafxpa35.json diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index df98bb0385f5b..32db4c9ad5fd5 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -16,6 +16,7 @@ "cl_3r8gc33pnqsxfe1g", # https://github.com/tuya/tuya-home-assistant/issues/754 "cl_cpbo62rn", # https://github.com/orgs/home-assistant/discussions/539 "cl_ebt12ypvexnixvtf", # https://github.com/tuya/tuya-home-assistant/issues/754 + "cl_g1cp07dsqnbdbbki", # https://github.com/home-assistant/core/issues/139966 "cl_qqdxfdht", # https://github.com/orgs/home-assistant/discussions/539 "cl_zah67ekd", # https://github.com/home-assistant/core/issues/71242 "clkg_nhyj64w2", # https://github.com/home-assistant/core/issues/136055 @@ -112,10 +113,12 @@ "dr_pjvxl1wsyqxivsaf", # https://github.com/home-assistant/core/issues/84869 "fs_g0ewlb1vmwqljzji", # https://github.com/home-assistant/core/issues/141231 "fs_ibytpo6fpnugft1c", # https://github.com/home-assistant/core/issues/135541 + "fsd_9ecs16c53uqskxw6", # https://github.com/home-assistant/core/issues/149233 "gyd_lgekqfxdabipm3tn", # https://github.com/home-assistant/core/issues/133173 "hps_2aaelwxk", # https://github.com/home-assistant/core/issues/149704 "hps_wqashyqo", # https://github.com/home-assistant/core/issues/146180 "hwsb_ircs2n82vgrozoew", # https://github.com/home-assistant/core/issues/149233 + "jtmspro_xqeob8h6", # https://github.com/orgs/home-assistant/discussions/517 "kg_4nqs33emdwJxpQ8O", # https://github.com/orgs/home-assistant/discussions/539 "kg_5ftkaulg", # https://github.com/orgs/home-assistant/discussions/539 "kg_gbm9ata1zrzaez4a", # https://github.com/home-assistant/core/issues/148347 @@ -179,6 +182,7 @@ "wk_aqoouq7x", # https://github.com/home-assistant/core/issues/146263 "wk_ccpwojhalfxryigz", # https://github.com/home-assistant/core/issues/145551 "wk_fi6dne5tu4t1nm6j", # https://github.com/orgs/home-assistant/discussions/243 + "wk_gc1bxoq2hafxpa35", # https://github.com/home-assistant/core/issues/145551 "wk_gogb05wrtredz3bs", # https://github.com/home-assistant/core/issues/136337 "wk_y5obtqhuztqsf2mj", # https://github.com/home-assistant/core/issues/139735 "wkcz_gc4b1mdw7kebtuyz", # https://github.com/home-assistant/core/issues/135617 diff --git a/tests/components/tuya/fixtures/cl_g1cp07dsqnbdbbki.json b/tests/components/tuya/fixtures/cl_g1cp07dsqnbdbbki.json new file mode 100644 index 0000000000000..85f4ec91d4b28 --- /dev/null +++ b/tests/components/tuya/fixtures/cl_g1cp07dsqnbdbbki.json @@ -0,0 +1,102 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Persiana do Quarto", + "category": "cl", + "product_id": "g1cp07dsqnbdbbki", + "product_name": "Smart roller blinds", + "online": true, + "sub": false, + "time_zone": "-03:00", + "active_time": "2023-06-21T04:29:09+00:00", + "create_time": "2023-06-21T04:29:09+00:00", + "update_time": "2023-06-21T04:29:09+00:00", + "function": { + "control": { + "type": "Enum", + "value": { + "range": ["open", "stop", "close", "continue"] + } + }, + "percent_control": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "control_back_mode": { + "type": "Enum", + "value": { + "range": ["forward", "back"] + } + }, + "border": { + "type": "Enum", + "value": { + "range": ["up", "down", "up_delete"] + } + } + }, + "status_range": { + "control": { + "type": "Enum", + "value": { + "range": ["open", "stop", "close", "continue"] + } + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["opening", "closing"] + } + }, + "percent_state": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "percent_control": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "control_back_mode": { + "type": "Enum", + "value": { + "range": ["forward", "back"] + } + }, + "border": { + "type": "Enum", + "value": { + "range": ["up", "down", "up_delete"] + } + } + }, + "status": { + "control": "open", + "work_state": "opening", + "percent_state": 0, + "percent_control": 100, + "control_back_mode": "back", + "border": "up" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/fsd_9ecs16c53uqskxw6.json b/tests/components/tuya/fixtures/fsd_9ecs16c53uqskxw6.json new file mode 100644 index 0000000000000..92c83e9e8f065 --- /dev/null +++ b/tests/components/tuya/fixtures/fsd_9ecs16c53uqskxw6.json @@ -0,0 +1,151 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "ceiling fan/Light v2", + "category": "fsd", + "product_id": "9ecs16c53uqskxw6", + "product_name": "ceiling fan/Light v2", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-07-08T17:05:22+00:00", + "create_time": "2025-07-08T17:05:22+00:00", + "update_time": "2025-07-08T17:05:22+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "fan_switch": { + "type": "Boolean", + "value": {} + }, + "fan_speed": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 6, + "scale": 0, + "step": 1 + } + }, + "fan_direction": { + "type": "Enum", + "value": { + "range": ["forward", "reverse"] + } + }, + "countdown_left_fan": { + "type": "Integer", + "value": { + "unit": "min", + "min": 0, + "max": 540, + "scale": 0, + "step": 1 + } + }, + "fan_beep": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "temp_value": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "scene_data": { + "type": "String", + "value": { + "maxlen": 255 + } + }, + "fan_switch": { + "type": "Boolean", + "value": {} + }, + "fan_speed": { + "type": "Integer", + "value": { + "unit": "", + "min": 1, + "max": 6, + "scale": 0, + "step": 1 + } + }, + "fan_direction": { + "type": "Enum", + "value": { + "range": ["forward", "reverse"] + } + }, + "countdown_left_fan": { + "type": "Integer", + "value": { + "unit": "min", + "min": 0, + "max": 540, + "scale": 0, + "step": 1 + } + }, + "fan_beep": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "temp_value": 0, + "scene_data": "", + "fan_switch": true, + "fan_speed": 2, + "fan_direction": "forward", + "countdown_left_fan": 0, + "fan_beep": false + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/jtmspro_xqeob8h6.json b/tests/components/tuya/fixtures/jtmspro_xqeob8h6.json new file mode 100644 index 0000000000000..e18b537c7e689 --- /dev/null +++ b/tests/components/tuya/fixtures/jtmspro_xqeob8h6.json @@ -0,0 +1,398 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "S1-TY-BLE-PRO 2", + "category": "jtmspro", + "product_id": "xqeob8h6", + "product_name": "S1-TY-BLE-PRO", + "online": false, + "sub": true, + "time_zone": "+03:00", + "active_time": "2025-07-01T15:21:36+00:00", + "create_time": "2025-07-01T15:21:36+00:00", + "update_time": "2025-07-01T15:21:36+00:00", + "function": { + "unlock_method_create": { + "type": "Raw", + "value": {} + }, + "unlock_method_delete": { + "type": "Raw", + "value": {} + }, + "unlock_method_modify": { + "type": "Raw", + "value": {} + }, + "lock_record": { + "type": "Raw", + "value": {} + }, + "message": { + "type": "Boolean", + "value": {} + }, + "automatic_lock": { + "type": "Boolean", + "value": {} + }, + "unlock_switch": { + "type": "Enum", + "value": { + "range": ["single_unlock", "finger_card"] + } + }, + "auto_lock_time": { + "type": "Integer", + "value": { + "min": 1, + "max": 1800, + "scale": 0, + "step": 1 + } + }, + "rtc_lock": { + "type": "Boolean", + "value": {} + }, + "manual_lock": { + "type": "Boolean", + "value": {} + }, + "synch_method": { + "type": "Raw", + "value": {} + }, + "remote_no_dp_key": { + "type": "Raw", + "value": {} + }, + "record": { + "type": "Raw", + "value": {} + }, + "check_code_set": { + "type": "Raw", + "value": {} + }, + "ble_unlock_check": { + "type": "Raw", + "value": {} + }, + "remote_pd_setkey_check": { + "type": "Raw", + "value": {} + }, + "unlock_ble_ibeacon": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "ibeacon_scan_mode": { + "type": "Enum", + "value": { + "range": [ + "always", + "5min", + "10min", + "20min", + "40min", + "60min", + "90min", + "120min" + ] + } + }, + "rssi_sensitivity_level": { + "type": "Enum", + "value": { + "range": [ + "inactive", + "90db", + "80db", + "70db", + "60db", + "50db", + "40db", + "30db", + "20db" + ] + } + }, + "ibeacon_switch": { + "type": "Boolean", + "value": {} + } + }, + "status_range": { + "unlock_method_create": { + "type": "Raw", + "value": {} + }, + "unlock_method_delete": { + "type": "Raw", + "value": {} + }, + "unlock_method_modify": { + "type": "Raw", + "value": {} + }, + "residual_electricity": { + "type": "Integer", + "value": { + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "unlock_fingerprint": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "unlock_card": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "unlock_key": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "unlock_ble": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "lock_record": { + "type": "Raw", + "value": {} + }, + "alarm_lock": { + "type": "Enum", + "value": { + "range": [ + "wrong_finger", + "wrong_password", + "wrong_card", + "wrong_face", + "tongue_bad", + "too_hot", + "unclosed_time", + "tongue_not_out", + "pry", + "key_in", + "low_battery", + "power_off", + "shock", + "defense" + ] + } + }, + "hijack": { + "type": "Boolean", + "value": {} + }, + "doorbell": { + "type": "Boolean", + "value": {} + }, + "message": { + "type": "Boolean", + "value": {} + }, + "automatic_lock": { + "type": "Boolean", + "value": {} + }, + "unlock_switch": { + "type": "Enum", + "value": { + "range": ["single_unlock", "finger_card"] + } + }, + "auto_lock_time": { + "type": "Integer", + "value": { + "min": 1, + "max": 1800, + "scale": 0, + "step": 1 + } + }, + "closed_opened": { + "type": "Enum", + "value": { + "range": ["unknown", "open", "closed"] + } + }, + "rtc_lock": { + "type": "Boolean", + "value": {} + }, + "manual_lock": { + "type": "Boolean", + "value": {} + }, + "lock_motor_state": { + "type": "Boolean", + "value": {} + }, + "synch_method": { + "type": "Raw", + "value": {} + }, + "remote_no_dp_key": { + "type": "Raw", + "value": {} + }, + "unlock_phone_remote": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "unlock_voice_remote": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "record": { + "type": "Raw", + "value": {} + }, + "check_code_set": { + "type": "Raw", + "value": {} + }, + "ble_unlock_check": { + "type": "Raw", + "value": {} + }, + "unlock_record_check": { + "type": "Raw", + "value": {} + }, + "remote_pd_setkey_check": { + "type": "Raw", + "value": {} + }, + "unlock_double_kit": { + "type": "Raw", + "value": {} + }, + "unlock_ble_ibeacon": { + "type": "Integer", + "value": { + "min": 0, + "max": 999, + "scale": 0, + "step": 1 + } + }, + "ibeacon_scan_mode": { + "type": "Enum", + "value": { + "range": [ + "always", + "5min", + "10min", + "20min", + "40min", + "60min", + "90min", + "120min" + ] + } + }, + "rssi_sensitivity_level": { + "type": "Enum", + "value": { + "range": [ + "inactive", + "90db", + "80db", + "70db", + "60db", + "50db", + "40db", + "30db", + "20db" + ] + } + }, + "ibeacon_switch": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "unlock_method_create": "A/8BAQIAAA==", + "unlock_method_delete": "", + "unlock_method_modify": "", + "residual_electricity": 99, + "unlock_fingerprint": 1, + "unlock_card": 0, + "unlock_key": 0, + "unlock_ble": 1, + "lock_record": "", + "alarm_lock": "wrong_finger", + "hijack": false, + "doorbell": false, + "message": false, + "automatic_lock": true, + "unlock_switch": "single_unlock", + "auto_lock_time": 3, + "closed_opened": "unknown", + "rtc_lock": false, + "manual_lock": true, + "lock_motor_state": false, + "synch_method": "AQA=", + "remote_no_dp_key": "AAAB", + "unlock_phone_remote": 1, + "unlock_voice_remote": 0, + "record": "AAEB", + "check_code_set": "AAH//wAAAAAAAAAAAP//AA==", + "ble_unlock_check": "AAH//zY2MDkzNTA0AWhkA/QAAA==", + "unlock_record_check": "", + "remote_pd_setkey_check": "AAH//zY2MDkzNTA0AQABAA==", + "unlock_double_kit": "", + "unlock_ble_ibeacon": 0, + "ibeacon_scan_mode": "always", + "rssi_sensitivity_level": "inactive", + "ibeacon_switch": false + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wk_gc1bxoq2hafxpa35.json b/tests/components/tuya/fixtures/wk_gc1bxoq2hafxpa35.json new file mode 100644 index 0000000000000..23a8607f2d1ea --- /dev/null +++ b/tests/components/tuya/fixtures/wk_gc1bxoq2hafxpa35.json @@ -0,0 +1,110 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "\u041f\u043e\u043b\u043e\u0442\u0435\u043d\u0446\u043e\u0441\u0443\u0448\u0438\u0442\u0435\u043b\u044c", + "category": "wk", + "product_id": "gc1bxoq2hafxpa35", + "product_name": "ET-44W", + "online": true, + "sub": false, + "time_zone": "+03:00", + "active_time": "2025-02-08T21:08:00+00:00", + "create_time": "2025-02-08T21:08:00+00:00", + "update_time": "2025-02-08T21:08:00+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["holiday"] + } + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 50, + "max": 400, + "scale": 1, + "step": 5 + } + }, + "temp_set_f": { + "type": "Integer", + "value": { + "unit": "\u2109", + "min": 410, + "max": 1040, + "scale": 1, + "step": 10 + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "mode": { + "type": "Enum", + "value": { + "range": ["holiday"] + } + }, + "temp_set": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 50, + "max": 400, + "scale": 1, + "step": 5 + } + }, + "temp_set_f": { + "type": "Integer", + "value": { + "unit": "\u2109", + "min": 410, + "max": 1040, + "scale": 1, + "step": 10 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": 0, + "max": 450, + "scale": 1, + "step": 5 + } + }, + "temp_current_f": { + "type": "Integer", + "value": { + "unit": "\u2109", + "min": 320, + "max": 1130, + "scale": 1, + "step": 10 + } + } + }, + "status": { + "switch": false, + "mode": "hold", + "temp_set": 50, + "temp_set_f": 410, + "temp_current": 253, + "temp_current_f": 320 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_climate.ambr b/tests/components/tuya/snapshots/test_climate.ambr index e075636be4f45..7687c68ad31f8 100644 --- a/tests/components/tuya/snapshots/test_climate.ambr +++ b/tests/components/tuya/snapshots/test_climate.ambr @@ -431,6 +431,79 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[climate.polotentsosushitel-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 40.0, + 'min_temp': 5.0, + 'preset_modes': list([ + 'holiday', + ]), + 'target_temp_step': 0.5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'climate', + 'entity_category': None, + 'entity_id': 'climate.polotentsosushitel', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.53apxfah2qoxb1cgkw', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[climate.polotentsosushitel-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_temperature': 25.3, + 'friendly_name': 'Полотенцосушитель', + 'hvac_modes': list([ + , + , + ]), + 'max_temp': 40.0, + 'min_temp': 5.0, + 'preset_mode': 'hold', + 'preset_modes': list([ + 'holiday', + ]), + 'supported_features': , + 'target_temp_step': 0.5, + 'temperature': 5.0, + }), + 'context': , + 'entity_id': 'climate.polotentsosushitel', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[climate.smart_thermostats-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_cover.ambr b/tests/components/tuya/snapshots/test_cover.ambr index 3266a5f65970e..0ba0911240879 100644 --- a/tests/components/tuya/snapshots/test_cover.ambr +++ b/tests/components/tuya/snapshots/test_cover.ambr @@ -254,6 +254,57 @@ 'state': 'open', }) # --- +# name: test_platform_setup_and_discovery[cover.persiana_do_quarto_curtain-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'cover', + 'entity_category': None, + 'entity_id': 'cover.persiana_do_quarto_curtain', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Curtain', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'curtain', + 'unique_id': 'tuya.ikbbdbnqsd70pc1glccontrol', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cover.persiana_do_quarto_curtain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 100, + 'device_class': 'curtain', + 'friendly_name': 'Persiana do Quarto Curtain', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.persiana_do_quarto_curtain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- # name: test_platform_setup_and_discovery[cover.tapparelle_studio_curtain-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_fan.ambr b/tests/components/tuya/snapshots/test_fan.ambr index 005420c205c92..f2b615ec26973 100644 --- a/tests/components/tuya/snapshots/test_fan.ambr +++ b/tests/components/tuya/snapshots/test_fan.ambr @@ -56,6 +56,64 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[fan.ceiling_fan_light_v2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': list([ + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.ceiling_fan_light_v2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': None, + 'unique_id': 'tuya.6wxksqu35c61sce9dsf', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[fan.ceiling_fan_light_v2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'direction': 'forward', + 'friendly_name': 'ceiling fan/Light v2', + 'percentage': 20, + 'percentage_step': 1.0, + 'preset_mode': None, + 'preset_modes': list([ + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.ceiling_fan_light_v2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[fan.ceiling_fan_with_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index 1e7ce8bdbffdf..9af474f875dfc 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -402,6 +402,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[53apxfah2qoxb1cgkw] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '53apxfah2qoxb1cgkw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'ET-44W', + 'model_id': 'gc1bxoq2hafxpa35', + 'name': 'Полотенцосушитель', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[53fnjncm3jywuaznps] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -588,6 +619,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[6h8boeqxorpsmtj] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '6h8boeqxorpsmtj', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'S1-TY-BLE-PRO (unsupported)', + 'model_id': 'xqeob8h6', + 'name': 'S1-TY-BLE-PRO 2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[6o148laaosbf0g4djd] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -650,6 +712,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[6wxksqu35c61sce9dsf] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + '6wxksqu35c61sce9dsf', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'ceiling fan/Light v2', + 'model_id': '9ecs16c53uqskxw6', + 'name': 'ceiling fan/Light v2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[73ov8i8iedtylkzrqzkfs] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -2882,6 +2975,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[ikbbdbnqsd70pc1glc] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'ikbbdbnqsd70pc1glc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart roller blinds', + 'model_id': 'g1cp07dsqnbdbbki', + 'name': 'Persiana do Quarto', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[iks13mcaiyie3rryjb2oc] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index 9abbf3c40f499..c18de2a228588 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -459,6 +459,87 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[light.ceiling_fan_light_v2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'light', + 'entity_category': None, + 'entity_id': 'light.ceiling_fan_light_v2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.6wxksqu35c61sce9dsfswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.ceiling_fan_light_v2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': , + 'color_temp': 500, + 'color_temp_kelvin': 2000, + 'friendly_name': 'ceiling fan/Light v2', + 'hs_color': tuple( + 30.601, + 94.547, + ), + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': tuple( + 255, + 137, + 14, + ), + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': tuple( + 0.598, + 0.383, + ), + }), + 'context': , + 'entity_id': 'light.ceiling_fan_light_v2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[light.ceiling_fan_with_light-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_select.ambr b/tests/components/tuya/snapshots/test_select.ambr index 08928a6440cae..fc238604ea3e4 100644 --- a/tests/components/tuya/snapshots/test_select.ambr +++ b/tests/components/tuya/snapshots/test_select.ambr @@ -3018,6 +3018,63 @@ 'state': 'last', }) # --- +# name: test_platform_setup_and_discovery[select.persiana_do_quarto_motor_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'forward', + 'back', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.persiana_do_quarto_motor_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Motor mode', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'curtain_motor_mode', + 'unique_id': 'tuya.ikbbdbnqsd70pc1glccontrol_back_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.persiana_do_quarto_motor_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Persiana do Quarto Motor mode', + 'options': list([ + 'forward', + 'back', + ]), + }), + 'context': , + 'entity_id': 'select.persiana_do_quarto_motor_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'back', + }) +# --- # name: test_platform_setup_and_discovery[select.raspy4_home_assistant_indicator_light_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 69757bed522974c2e58072eb24e623683aeabac4 Mon Sep 17 00:00:00 2001 From: Matrix Date: Tue, 19 Aug 2025 17:35:36 +0800 Subject: [PATCH 02/23] Support for YoLink YS4102 YS4103 (#150464) --- homeassistant/components/yolink/__init__.py | 1 + .../components/yolink/coordinator.py | 18 ++- homeassistant/components/yolink/entity.py | 14 +-- homeassistant/components/yolink/icons.json | 10 ++ homeassistant/components/yolink/select.py | 119 ++++++++++++++++++ homeassistant/components/yolink/sensor.py | 5 + homeassistant/components/yolink/strings.json | 13 ++ homeassistant/components/yolink/valve.py | 50 +++++++- 8 files changed, 215 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/yolink/select.py diff --git a/homeassistant/components/yolink/__init__.py b/homeassistant/components/yolink/__init__.py index 96db2ab555a3e..f33da34c1fc9d 100644 --- a/homeassistant/components/yolink/__init__.py +++ b/homeassistant/components/yolink/__init__.py @@ -43,6 +43,7 @@ Platform.LIGHT, Platform.LOCK, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SIREN, Platform.SWITCH, diff --git a/homeassistant/components/yolink/coordinator.py b/homeassistant/components/yolink/coordinator.py index 7d5323663de2b..2c914e84a08c6 100644 --- a/homeassistant/components/yolink/coordinator.py +++ b/homeassistant/components/yolink/coordinator.py @@ -5,13 +5,16 @@ import asyncio from datetime import UTC, datetime, timedelta import logging +from typing import Any +from yolink.client_request import ClientRequest from yolink.device import YoLinkDevice from yolink.exception import YoLinkAuthFailError, YoLinkClientError +from yolink.model import BRDP from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ATTR_DEVICE_STATE, ATTR_LORA_INFO, DOMAIN, YOLINK_OFFLINE_TIME @@ -89,3 +92,16 @@ async def _async_update_data(self) -> dict: self.dev_net_type = dev_lora_info.get("devNetType") return device_state return {} + + async def call_device(self, request: ClientRequest) -> dict[str, Any]: + """Call device api.""" + try: + # call_device will check result, fail by raise YoLinkClientError + resp: BRDP = await self.device.call_device(request) + except YoLinkAuthFailError as yl_auth_err: + self.config_entry.async_start_reauth(self.hass) + raise HomeAssistantError(yl_auth_err) from yl_auth_err + except YoLinkClientError as yl_client_err: + raise HomeAssistantError(yl_client_err) from yl_client_err + else: + return resp.data diff --git a/homeassistant/components/yolink/entity.py b/homeassistant/components/yolink/entity.py index 7828bf91541e2..ecc42ad1a0eb0 100644 --- a/homeassistant/components/yolink/entity.py +++ b/homeassistant/components/yolink/entity.py @@ -3,13 +3,12 @@ from __future__ import annotations from abc import abstractmethod +from typing import Any from yolink.client_request import ClientRequest -from yolink.exception import YoLinkAuthFailError, YoLinkClientError from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -64,13 +63,6 @@ def device_info(self) -> DeviceInfo: def update_entity_state(self, state: dict) -> None: """Parse and update entity state, should be overridden.""" - async def call_device(self, request: ClientRequest) -> None: + async def call_device(self, request: ClientRequest) -> dict[str, Any]: """Call device api.""" - try: - # call_device will check result, fail by raise YoLinkClientError - await self.coordinator.device.call_device(request) - except YoLinkAuthFailError as yl_auth_err: - self.config_entry.async_start_reauth(self.hass) - raise HomeAssistantError(yl_auth_err) from yl_auth_err - except YoLinkClientError as yl_client_err: - raise HomeAssistantError(yl_client_err) from yl_client_err + return await self.coordinator.call_device(request) diff --git a/homeassistant/components/yolink/icons.json b/homeassistant/components/yolink/icons.json index 6d9062a92b841..59366b804f599 100644 --- a/homeassistant/components/yolink/icons.json +++ b/homeassistant/components/yolink/icons.json @@ -27,10 +27,20 @@ "default": "mdi:gauge" } }, + "select": { + "sprinkler_mode": { + "default": "mdi:auto-mode" + } + }, "switch": { "manipulator_state": { "default": "mdi:pipe" } + }, + "valve": { + "sprinkler_valve": { + "default": "mdi:sprinkler-variant" + } } }, "services": { diff --git a/homeassistant/components/yolink/select.py b/homeassistant/components/yolink/select.py new file mode 100644 index 0000000000000..e98b0440b9257 --- /dev/null +++ b/homeassistant/components/yolink/select.py @@ -0,0 +1,119 @@ +"""YoLink select platform.""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from typing import Any + +from yolink.client_request import ClientRequest +from yolink.const import ATTR_DEVICE_SPRINKLER +from yolink.device import YoLinkDevice +from yolink.message_resolver import sprinkler_message_resolve + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import DOMAIN +from .coordinator import YoLinkCoordinator +from .entity import YoLinkEntity + + +@dataclass(frozen=True, kw_only=True) +class YoLinkSelectEntityDescription(SelectEntityDescription): + """YoLink SelectEntityDescription.""" + + state_key: str = "state" + exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True + should_update_entity: Callable = lambda state: True + value: Callable = lambda data: data + on_option_selected: Callable[[YoLinkCoordinator, str], Awaitable[bool]] + + +async def set_sprinker_mode_fn(coordinator: YoLinkCoordinator, option: str) -> bool: + """Set sprinkler mode.""" + data: dict[str, Any] = await coordinator.call_device( + ClientRequest( + "setState", + { + "state": { + "mode": option, + } + }, + ) + ) + sprinkler_message_resolve(coordinator.device, data, None) + coordinator.async_set_updated_data(data) + return True + + +SELECTOR_MAPPINGS: tuple[YoLinkSelectEntityDescription, ...] = ( + YoLinkSelectEntityDescription( + key="model", + options=["auto", "manual", "off"], + translation_key="sprinkler_mode", + value=lambda data: ( + data.get("mode") if data is not None else None + ), # watering state report will missing state field + exists_fn=lambda device: device.device_type == ATTR_DEVICE_SPRINKLER, + should_update_entity=lambda value: value is not None, + on_option_selected=set_sprinker_mode_fn, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up YoLink select from a config entry.""" + device_coordinators = hass.data[DOMAIN][config_entry.entry_id].device_coordinators + async_add_entities( + YoLinkSelectEntity(config_entry, selector_device_coordinator, description) + for selector_device_coordinator in device_coordinators.values() + if selector_device_coordinator.device.device_type in [ATTR_DEVICE_SPRINKLER] + for description in SELECTOR_MAPPINGS + if description.exists_fn(selector_device_coordinator.device) + ) + + +class YoLinkSelectEntity(YoLinkEntity, SelectEntity): + """YoLink Select Entity.""" + + entity_description: YoLinkSelectEntityDescription + + def __init__( + self, + config_entry: ConfigEntry, + coordinator: YoLinkCoordinator, + description: YoLinkSelectEntityDescription, + ) -> None: + """Init YoLink Select.""" + super().__init__(config_entry, coordinator) + self.entity_description = description + self._attr_unique_id = ( + f"{coordinator.device.device_id} {self.entity_description.key}" + ) + + @callback + def update_entity_state(self, state: dict[str, Any]) -> None: + """Update HA Entity State.""" + if ( + current_value := self.entity_description.value( + state.get(self.entity_description.state_key) + ) + ) is None and self.entity_description.should_update_entity( + current_value + ) is False: + return + self._attr_current_option = current_value + self.async_write_ha_state() + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + if await self.entity_description.on_option_selected(self.coordinator, option): + self._attr_current_option = option + self.async_write_ha_state() diff --git a/homeassistant/components/yolink/sensor.py b/homeassistant/components/yolink/sensor.py index 5425c242821f1..3bb0e965eae5b 100644 --- a/homeassistant/components/yolink/sensor.py +++ b/homeassistant/components/yolink/sensor.py @@ -23,6 +23,8 @@ ATTR_DEVICE_SMART_REMOTER, ATTR_DEVICE_SMOKE_ALARM, ATTR_DEVICE_SOIL_TH_SENSOR, + ATTR_DEVICE_SPRINKLER, + ATTR_DEVICE_SPRINKLER_V2, ATTR_DEVICE_SWITCH, ATTR_DEVICE_TH_SENSOR, ATTR_DEVICE_THERMOSTAT, @@ -110,6 +112,8 @@ class YoLinkSensorEntityDescription(SensorEntityDescription): ATTR_GARAGE_DOOR_CONTROLLER, ATTR_DEVICE_SOIL_TH_SENSOR, ATTR_DEVICE_SMOKE_ALARM, + ATTR_DEVICE_SPRINKLER, + ATTR_DEVICE_SPRINKLER_V2, ] BATTERY_POWER_SENSOR = [ @@ -131,6 +135,7 @@ class YoLinkSensorEntityDescription(SensorEntityDescription): ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, ATTR_DEVICE_SOIL_TH_SENSOR, ATTR_DEVICE_SMOKE_ALARM, + ATTR_DEVICE_SPRINKLER_V2, ] MCU_DEV_TEMPERATURE_SENSOR = [ diff --git a/homeassistant/components/yolink/strings.json b/homeassistant/components/yolink/strings.json index 4215031d90497..9e60b77f43aa1 100644 --- a/homeassistant/components/yolink/strings.json +++ b/homeassistant/components/yolink/strings.json @@ -121,6 +121,19 @@ }, "meter_valve_2_state": { "name": "Valve 2" + }, + "sprinkler_valve": { + "name": "[%key:component::valve::title%]" + } + }, + "select": { + "sprinkler_mode": { + "name": "Mode", + "state": { + "auto": "[%key:common::state::auto%]", + "manual": "[%key:common::state::manual%]", + "off": "[%key:common::state::off%]" + } } } }, diff --git a/homeassistant/components/yolink/valve.py b/homeassistant/components/yolink/valve.py index e63488194d08a..8361724a3cfd0 100644 --- a/homeassistant/components/yolink/valve.py +++ b/homeassistant/components/yolink/valve.py @@ -4,11 +4,14 @@ from collections.abc import Callable from dataclasses import dataclass +from typing import Any from yolink.client_request import ClientRequest from yolink.const import ( ATTR_DEVICE_MODEL_A, ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, + ATTR_DEVICE_SPRINKLER, + ATTR_DEVICE_SPRINKLER_V2, ATTR_DEVICE_WATER_METER_CONTROLLER, ) from yolink.device import YoLinkDevice @@ -36,6 +39,20 @@ class YoLinkValveEntityDescription(ValveEntityDescription): exists_fn: Callable[[YoLinkDevice], bool] = lambda _: True value: Callable = lambda state: state channel_index: int | None = None + should_update_entity: Callable = lambda state: True + is_available: Callable[[YoLinkDevice, dict[str, Any]], bool] = ( + lambda device, state: True + ) + + +def sprinkler_valve_available(device: YoLinkDevice, data: dict[str, Any]) -> bool: + """Check if sprinkler valve is available.""" + if device.device_type == ATTR_DEVICE_SPRINKLER_V2: + return True + if (state := data.get("state")) is not None: + if (mode := state.get("mode")) is not None: + return mode == "manual" + return False DEVICE_TYPES: tuple[YoLinkValveEntityDescription, ...] = ( @@ -68,11 +85,24 @@ class YoLinkValveEntityDescription(ValveEntityDescription): ), channel_index=1, ), + YoLinkValveEntityDescription( + key="valve", + translation_key="sprinkler_valve", + device_class=ValveDeviceClass.WATER, + value=lambda value: value is False if value is not None else None, + exists_fn=lambda device: ( + device.device_type in [ATTR_DEVICE_SPRINKLER, ATTR_DEVICE_SPRINKLER_V2] + ), + should_update_entity=lambda value: value is not None, + is_available=sprinkler_valve_available, + ), ) DEVICE_TYPE = [ ATTR_DEVICE_WATER_METER_CONTROLLER, ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER, + ATTR_DEVICE_SPRINKLER, + ATTR_DEVICE_SPRINKLER_V2, ] @@ -124,9 +154,13 @@ def update_entity_state(self, state: dict[str, str | list[str]]) -> None: attr_val := self.entity_description.value( state.get(self.entity_description.key) ) - ) is None: + ) is None and self.entity_description.should_update_entity(attr_val) is False: return - self._attr_is_closed = attr_val + if self.entity_description.is_available(self.coordinator.device, state) is True: + self._attr_is_closed = attr_val + self._attr_available = True + else: + self._attr_available = False self.async_write_ha_state() async def _async_invoke_device(self, state: str) -> None: @@ -147,6 +181,16 @@ async def _async_invoke_device(self, state: str) -> None: await self.call_device( ClientRequest("setState", {"valves": {str(channel_index): state}}) ) + if self.coordinator.device.device_type == ATTR_DEVICE_SPRINKLER: + await self.call_device( + ClientRequest( + "setManualWater", {"state": "start" if state == "open" else "stop"} + ) + ) + if self.coordinator.device.device_type == ATTR_DEVICE_SPRINKLER_V2: + await self.call_device( + ClientRequest("setState", {"running": state == "open"}) + ) else: await self.call_device(ClientRequest("setState", {"valve": state})) self._attr_is_closed = state == "close" @@ -163,4 +207,4 @@ async def async_close_valve(self) -> None: @property def available(self) -> bool: """Return true is device is available.""" - return super().available + return self._attr_available and super().available From c18dc9b63b68bc83a02f0ac7b708a0cfdaf81314 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:19:10 +0200 Subject: [PATCH 03/23] Fix icloud service calls (#150881) --- homeassistant/components/icloud/services.py | 25 +++++++++------------ 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/icloud/services.py b/homeassistant/components/icloud/services.py index dbb843e821681..44a2e5d52f7a6 100644 --- a/homeassistant/components/icloud/services.py +++ b/homeassistant/components/icloud/services.py @@ -8,7 +8,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.util import slugify -from .account import IcloudAccount +from .account import IcloudAccount, IcloudConfigEntry from .const import ( ATTR_ACCOUNT, ATTR_DEVICE_NAME, @@ -92,8 +92,10 @@ def lost_device(service: ServiceCall) -> None: def update_account(service: ServiceCall) -> None: """Call the update function of an iCloud account.""" if (account := service.data.get(ATTR_ACCOUNT)) is None: - for account in service.hass.data[DOMAIN].values(): - account.keep_alive() + # Update all accounts when no specific account is provided + entry: IcloudConfigEntry + for entry in service.hass.config_entries.async_loaded_entries(DOMAIN): + entry.runtime_data.keep_alive() else: _get_account(service.hass, account).keep_alive() @@ -102,17 +104,12 @@ def _get_account(hass: HomeAssistant, account_identifier: str) -> IcloudAccount: if account_identifier is None: return None - icloud_account: IcloudAccount | None = hass.data[DOMAIN].get(account_identifier) - if icloud_account is None: - for account in hass.data[DOMAIN].values(): - if account.username == account_identifier: - icloud_account = account - - if icloud_account is None: - raise ValueError( - f"No iCloud account with username or name {account_identifier}" - ) - return icloud_account + entry: IcloudConfigEntry + for entry in hass.config_entries.async_loaded_entries(DOMAIN): + if entry.runtime_data.username == account_identifier: + return entry.runtime_data + + raise ValueError(f"No iCloud account with username or name {account_identifier}") @callback From c62c52e8cfe969f2c56cf31ae2bce00ede3fa76d Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Tue, 19 Aug 2025 13:19:44 +0100 Subject: [PATCH 04/23] Bump mastodon.py to 2.1.1 (#150876) --- homeassistant/components/mastodon/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index 99bb9801183a7..4632ee1eff6e8 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["mastodon"], "quality_scale": "bronze", - "requirements": ["Mastodon.py==2.1.0"] + "requirements": ["Mastodon.py==2.1.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 51b620117cdfa..17052c94bf139 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -22,7 +22,7 @@ HAP-python==4.9.2 HATasmota==0.10.0 # homeassistant.components.mastodon -Mastodon.py==2.1.0 +Mastodon.py==2.1.1 # homeassistant.components.playstation_network PSNAWP==3.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 888e57c5c7281..669695558d9dd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -22,7 +22,7 @@ HAP-python==4.9.2 HATasmota==0.10.0 # homeassistant.components.mastodon -Mastodon.py==2.1.0 +Mastodon.py==2.1.1 # homeassistant.components.playstation_network PSNAWP==3.0.0 From 69dbcb062709d7e03db22f27a5c5401c52d8506f Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:20:05 +0200 Subject: [PATCH 05/23] Add sound switch to Tuya fan light (#150879) --- homeassistant/components/tuya/switch.py | 9 ++++ .../tuya/snapshots/test_switch.ambr | 48 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index e20923133f93b..b9edc82ad71b2 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -227,6 +227,15 @@ entity_category=EntityCategory.CONFIG, ), ), + # Ceiling Fan Light + # https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v + "fsd": ( + SwitchEntityDescription( + key=DPCode.FAN_BEEP, + translation_key="sound", + entity_category=EntityCategory.CONFIG, + ), + ), # Irrigator # https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k "ggq": ( diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 8e139b64876e5..147c18e9e2a88 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -2369,6 +2369,54 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[switch.ceiling_fan_light_v2_sound-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ceiling_fan_light_v2_sound', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sound', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sound', + 'unique_id': 'tuya.6wxksqu35c61sce9dsffan_beep', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.ceiling_fan_light_v2_sound-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'ceiling fan/Light v2 Sound', + }), + 'context': , + 'entity_id': 'switch.ceiling_fan_light_v2_sound', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[switch.clima_cucina_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From e6a158b1acd4a78e2d0bc2f093306d1965055d6d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:20:29 +0200 Subject: [PATCH 06/23] Add temperature sensor to Tuya solar inverters (#150878) --- homeassistant/components/tuya/sensor.py | 6 + tests/components/tuya/__init__.py | 1 + .../tuya/fixtures/znnbq_6b3pbbuqbfabhfiq.json | 56 ++++++ .../components/tuya/snapshots/test_init.ambr | 31 ++++ .../tuya/snapshots/test_sensor.ambr | 171 ++++++++++++++++++ 5 files changed, 265 insertions(+) create mode 100644 tests/components/tuya/fixtures/znnbq_6b3pbbuqbfabhfiq.json diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 48543f2cd48f6..fe7db2b28b965 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -1505,6 +1505,12 @@ class TuyaSensorEntityDescription(SensorEntityDescription): suggested_display_precision=0, suggested_unit_of_measurement=UnitOfPower.WATT, ), + TuyaSensorEntityDescription( + key=DPCode.TEMP_CURRENT, + translation_key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), ), # Pool HeatPump "znrb": ( diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index 32db4c9ad5fd5..246a4388c7668 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -212,6 +212,7 @@ "zndb_4ggkyflayu1h1ho9", # https://github.com/home-assistant/core/pull/149317 "zndb_v5jlnn5hwyffkhp3", # https://github.com/home-assistant/core/issues/143209 "zndb_ze8faryrxr0glqnn", # https://github.com/home-assistant/core/issues/138372 + "znnbq_6b3pbbuqbfabhfiq", # https://github.com/orgs/home-assistant/discussions/707 "znrb_db81ge24jctwx8lo", # https://github.com/home-assistant/core/issues/136513 "zwjcy_myd45weu", # https://github.com/orgs/home-assistant/discussions/482 ] diff --git a/tests/components/tuya/fixtures/znnbq_6b3pbbuqbfabhfiq.json b/tests/components/tuya/fixtures/znnbq_6b3pbbuqbfabhfiq.json new file mode 100644 index 0000000000000..597721599e3ca --- /dev/null +++ b/tests/components/tuya/fixtures/znnbq_6b3pbbuqbfabhfiq.json @@ -0,0 +1,56 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Wi-Fi solar grid micro inverter \uff08GT\uff09", + "category": "znnbq", + "product_id": "6b3pbbuqbfabhfiq", + "product_name": "Wi-Fi solar grid micro inverter \uff08GT\uff09", + "online": true, + "sub": false, + "time_zone": "+02:00", + "active_time": "2025-08-18T16:19:32+00:00", + "create_time": "2025-08-18T16:19:32+00:00", + "update_time": "2025-08-18T16:19:32+00:00", + "function": {}, + "status_range": { + "reverse_energy_total": { + "type": "Integer", + "value": { + "unit": "kW\u00b7h", + "min": 0, + "max": 99999999, + "scale": 2, + "step": 1 + } + }, + "power_total": { + "type": "Integer", + "value": { + "unit": "kW", + "min": 0, + "max": 900000, + "scale": 3, + "step": 1 + } + }, + "temp_current": { + "type": "Integer", + "value": { + "unit": "\u2103", + "min": -500, + "max": 2000, + "scale": 1, + "step": 1 + } + } + }, + "status": { + "reverse_energy_total": 19, + "power_total": 0, + "temp_current": 219 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index 9af474f875dfc..5eb43abd365f7 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -4556,6 +4556,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[qifhbafbqubbp3b6qbnnz] + DeviceRegistryEntrySnapshot({ + 'area_id': None, + 'config_entries': , + 'config_entries_subentries': , + 'configuration_url': None, + 'connections': set({ + }), + 'disabled_by': None, + 'entry_type': None, + 'hw_version': None, + 'id': , + 'identifiers': set({ + tuple( + 'tuya', + 'qifhbafbqubbp3b6qbnnz', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Wi-Fi solar grid micro inverter (GT)', + 'model_id': '6b3pbbuqbfabhfiq', + 'name': 'Wi-Fi solar grid micro inverter (GT)', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[queafegmhhmtivdxjd] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index b2c0b92bd30de..0baf85f05b636 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -13123,6 +13123,177 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power', + 'unique_id': 'tuya.qifhbafbqubbp3b6qbnnzpower_total', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Wi-Fi solar grid micro inverter (GT) Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'temperature', + 'unique_id': 'tuya.qifhbafbqubbp3b6qbnnztemp_current', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Wi-Fi solar grid micro inverter (GT) Temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.9', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_total_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_total_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Total energy', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_energy', + 'unique_id': 'tuya.qifhbafbqubbp3b6qbnnzreverse_energy_total', + 'unit_of_measurement': , + }) +# --- +# name: test_platform_setup_and_discovery[sensor.wi_fi_solar_grid_micro_inverter_gt_total_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Wi-Fi solar grid micro inverter (GT) Total energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.wi_fi_solar_grid_micro_inverter_gt_total_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.19', + }) +# --- # name: test_platform_setup_and_discovery[sensor.wifi_smart_gas_boiler_thermostat_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 4c1788e757c76eb3f8ecb034024379fb5a11231d Mon Sep 17 00:00:00 2001 From: Imeon-Energy Date: Tue, 19 Aug 2025 14:21:33 +0200 Subject: [PATCH 07/23] Add sensors to Imeon inverter integration (#146437) Co-authored-by: TheBushBoy Co-authored-by: Joost Lekkerkerker --- .../components/imeon_inverter/const.py | 23 + .../components/imeon_inverter/coordinator.py | 6 +- .../components/imeon_inverter/icons.json | 49 +- .../components/imeon_inverter/sensor.py | 77 + .../components/imeon_inverter/strings.json | 72 +- tests/components/imeon_inverter/conftest.py | 2 +- .../imeon_inverter/fixtures/entity_data.json | 79 + .../imeon_inverter/fixtures/sensor_data.json | 73 - .../imeon_inverter/snapshots/test_sensor.ambr | 1516 ++++++++++++----- .../components/imeon_inverter/test_sensor.py | 58 +- 10 files changed, 1405 insertions(+), 550 deletions(-) create mode 100644 tests/components/imeon_inverter/fixtures/entity_data.json delete mode 100644 tests/components/imeon_inverter/fixtures/sensor_data.json diff --git a/homeassistant/components/imeon_inverter/const.py b/homeassistant/components/imeon_inverter/const.py index fd08955c038e3..9cde40e01d79c 100644 --- a/homeassistant/components/imeon_inverter/const.py +++ b/homeassistant/components/imeon_inverter/const.py @@ -7,3 +7,26 @@ PLATFORMS = [ Platform.SENSOR, ] +ATTR_BATTERY_STATUS = ["charging", "discharging", "charged"] +ATTR_INVERTER_STATE = [ + "unsynchronized", + "grid_consumption", + "grid_injection", + "grid_synchronised_but_not_used", +] +ATTR_TIMELINE_STATUS = [ + "com_lost", + "warning_grid", + "warning_pv", + "warning_bat", + "error_ond", + "error_soft", + "error_pv", + "error_grid", + "error_bat", + "good_1", + "info_soft", + "info_ond", + "info_bat", + "info_smartlo", +] diff --git a/homeassistant/components/imeon_inverter/coordinator.py b/homeassistant/components/imeon_inverter/coordinator.py index f1963a45579d1..d41e9fd43b2af 100644 --- a/homeassistant/components/imeon_inverter/coordinator.py +++ b/homeassistant/components/imeon_inverter/coordinator.py @@ -17,7 +17,7 @@ from .const import TIMEOUT HUBNAME = "imeon_inverter_hub" -INTERVAL = timedelta(seconds=60) +INTERVAL = 60 _LOGGER = logging.getLogger(__name__) type InverterConfigEntry = ConfigEntry[InverterCoordinator] @@ -44,7 +44,7 @@ def __init__( hass, _LOGGER, name=HUBNAME, - update_interval=INTERVAL, + update_interval=timedelta(seconds=INTERVAL), config_entry=entry, ) @@ -83,7 +83,7 @@ async def _async_update_data(self) -> dict[str, str | float | int]: # Fetch data using distant API try: await self._api.update() - except (ValueError, ClientError) as e: + except (ValueError, TimeoutError, ClientError) as e: raise UpdateFailed(e) from e # Store data diff --git a/homeassistant/components/imeon_inverter/icons.json b/homeassistant/components/imeon_inverter/icons.json index 6ede2416afae8..a4a7edf21a63a 100644 --- a/homeassistant/components/imeon_inverter/icons.json +++ b/homeassistant/components/imeon_inverter/icons.json @@ -7,8 +7,14 @@ "battery_soc": { "default": "mdi:battery-charging-100" }, + "battery_status": { + "default": "mdi:battery-alert" + }, "battery_stored": { - "default": "mdi:battery" + "default": "mdi:battery-arrow-up" + }, + "battery_consumed": { + "default": "mdi:battery-arrow-down" }, "grid_current_l1": { "default": "mdi:current-ac" @@ -50,7 +56,7 @@ "default": "mdi:power-socket" }, "meter_power": { - "default": "mdi:power-plug" + "default": "mdi:meter-electric" }, "output_current_l1": { "default": "mdi:current-ac" @@ -116,7 +122,7 @@ "default": "mdi:home-lightning-bolt" }, "monitoring_minute_grid_consumption": { - "default": "mdi:transmission-tower" + "default": "mdi:transmission-tower-import" }, "monitoring_minute_grid_injection": { "default": "mdi:transmission-tower-export" @@ -126,6 +132,43 @@ }, "monitoring_minute_solar_production": { "default": "mdi:solar-power" + }, + "timeline_type_msg": { + "default": "mdi:check-circle", + "state": { + "com_lost": "mdi:lan-disconnect", + "warning_grid": "mdi:alert-circle", + "warning_pv": "mdi:alert-circle", + "warning_bat": "mdi:alert-circle", + "error_ond": "mdi:close-octagon", + "error_soft": "mdi:close-octagon", + "error_pv": "mdi:close-octagon", + "error_grid": "mdi:close-octagon", + "error_bat": "mdi:close-octagon", + "good_1": "mdi:check-circle", + "info_soft": "mdi:information-slab-circle", + "info_ond": "mdi:information-slab-circle", + "info_bat": "mdi:information-slab-circle", + "info_smartlo": "mdi:information-slab-circle" + } + }, + "energy_pv": { + "default": "mdi:solar-power" + }, + "energy_grid_injected": { + "default": "mdi:transmission-tower-export" + }, + "energy_grid_consumed": { + "default": "mdi:transmission-tower-import" + }, + "energy_building_consumption": { + "default": "mdi:home-lightning-bolt-outline" + }, + "energy_battery_stored": { + "default": "mdi:battery-arrow-up-outline" + }, + "energy_battery_consumed": { + "default": "mdi:battery-arrow-down-outline" } } } diff --git a/homeassistant/components/imeon_inverter/sensor.py b/homeassistant/components/imeon_inverter/sensor.py index 119677c0a8a3c..21aa37a052362 100644 --- a/homeassistant/components/imeon_inverter/sensor.py +++ b/homeassistant/components/imeon_inverter/sensor.py @@ -14,6 +14,7 @@ EntityCategory, UnitOfElectricCurrent, UnitOfElectricPotential, + UnitOfEnergy, UnitOfFrequency, UnitOfPower, UnitOfTemperature, @@ -22,6 +23,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType +from .const import ATTR_BATTERY_STATUS, ATTR_INVERTER_STATE, ATTR_TIMELINE_STATUS from .coordinator import InverterCoordinator from .entity import InverterEntity @@ -46,6 +48,12 @@ device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="battery_status", + translation_key="battery_status", + device_class=SensorDeviceClass.ENUM, + options=ATTR_BATTERY_STATUS, + ), SensorEntityDescription( key="battery_stored", translation_key="battery_stored", @@ -53,6 +61,13 @@ device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="battery_consumed", + translation_key="battery_consumed", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), # Grid SensorEntityDescription( key="grid_current_l1", @@ -147,6 +162,12 @@ device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, ), + SensorEntityDescription( + key="manager_inverter_state", + translation_key="manager_inverter_state", + device_class=SensorDeviceClass.ENUM, + options=ATTR_INVERTER_STATE, + ), # Meter SensorEntityDescription( key="meter_power", @@ -340,6 +361,62 @@ state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, ), + # Timeline + SensorEntityDescription( + key="timeline_type_msg", + translation_key="timeline_type_msg", + device_class=SensorDeviceClass.ENUM, + options=ATTR_TIMELINE_STATUS, + ), + # Daily energy counters + SensorEntityDescription( + key="energy_pv", + translation_key="energy_pv", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="energy_grid_injected", + translation_key="energy_grid_injected", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="energy_grid_consumed", + translation_key="energy_grid_consumed", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="energy_building_consumption", + translation_key="energy_building_consumption", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="energy_battery_stored", + translation_key="energy_battery_stored", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), + SensorEntityDescription( + key="energy_battery_consumed", + translation_key="energy_battery_consumed", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=2, + ), ) diff --git a/homeassistant/components/imeon_inverter/strings.json b/homeassistant/components/imeon_inverter/strings.json index 86855361b8fa0..66d0472b89ae9 100644 --- a/homeassistant/components/imeon_inverter/strings.json +++ b/homeassistant/components/imeon_inverter/strings.json @@ -35,9 +35,20 @@ "battery_soc": { "name": "Battery state of charge" }, + "battery_status": { + "name": "Battery status", + "state": { + "charged": "Charged", + "charging": "[%key:common::state::charging%]", + "discharging": "[%key:common::state::discharging%]" + } + }, "battery_stored": { "name": "Battery stored" }, + "battery_consumed": { + "name": "Battery consumed" + }, "grid_current_l1": { "name": "Grid current L1" }, @@ -77,6 +88,15 @@ "inverter_injection_power_limit": { "name": "Injection power limit" }, + "manager_inverter_state": { + "name": "Inverter state", + "state": { + "unsynchronized": "Unsynchronized", + "grid_consumption": "Grid consumption", + "grid_injection": "Grid injection", + "grid_synchronised_but_not_used": "Grid unsynchronized but used" + } + }, "meter_power": { "name": "Meter power" }, @@ -135,25 +155,63 @@ "name": "Component temperature" }, "monitoring_self_consumption": { - "name": "Monitoring self-consumption" + "name": "Self-consumption" }, "monitoring_self_sufficiency": { - "name": "Monitoring self-sufficiency" + "name": "Self-sufficiency" }, "monitoring_minute_building_consumption": { - "name": "Monitoring building consumption (minute)" + "name": "Building consumption" }, "monitoring_minute_grid_consumption": { - "name": "Monitoring grid consumption (minute)" + "name": "Grid consumption" }, "monitoring_minute_grid_injection": { - "name": "Monitoring grid injection (minute)" + "name": "Grid injection" }, "monitoring_minute_grid_power_flow": { - "name": "Monitoring grid power flow (minute)" + "name": "Grid power flow" }, "monitoring_minute_solar_production": { - "name": "Monitoring solar production (minute)" + "name": "Solar production" + }, + "timeline_type_msg": { + "name": "Timeline status", + "state": { + "com_lost": "Communication lost.", + "warning_grid": "Power grid warning detected.", + "warning_pv": "PV system warning detected.", + "warning_bat": "Battery warning detected.", + "error_ond": "Inverter error detected.", + "error_soft": "Software error detected.", + "error_pv": "PV system error detected.", + "error_grid": "Power grid error detected.", + "error_bat": "Battery error detected.", + "good_1": "System operating normally.", + "web_account": "Web account notification.", + "info_soft": "Software information available.", + "info_ond": "Inverter information available.", + "info_bat": "Battery information available.", + "info_smartlo": "Smart load information available." + } + }, + "energy_pv": { + "name": "Today PV energy" + }, + "energy_grid_injected": { + "name": "Today grid-injected energy" + }, + "energy_grid_consumed": { + "name": "Today grid-consumed energy" + }, + "energy_building_consumption": { + "name": "Today building consumption" + }, + "energy_battery_stored": { + "name": "Today battery-stored energy" + }, + "energy_battery_consumed": { + "name": "Today battery-consumed energy" } } } diff --git a/tests/components/imeon_inverter/conftest.py b/tests/components/imeon_inverter/conftest.py index e147a6ff642a2..37fa47e7afbd4 100644 --- a/tests/components/imeon_inverter/conftest.py +++ b/tests/components/imeon_inverter/conftest.py @@ -66,7 +66,7 @@ def mock_imeon_inverter() -> Generator[MagicMock]: "serial": TEST_SERIAL, "url": f"http://{TEST_USER_INPUT[CONF_HOST]}", } - inverter.storage = load_json_object_fixture("sensor_data.json", DOMAIN) + inverter.storage = load_json_object_fixture("entity_data.json", DOMAIN) yield inverter diff --git a/tests/components/imeon_inverter/fixtures/entity_data.json b/tests/components/imeon_inverter/fixtures/entity_data.json new file mode 100644 index 0000000000000..a2717f093f4e3 --- /dev/null +++ b/tests/components/imeon_inverter/fixtures/entity_data.json @@ -0,0 +1,79 @@ +{ + "battery": { + "battery_power": 2500.0, + "battery_soc": 78.0, + "battery_status": "charging", + "battery_stored": 10200.0, + "battery_consumed": 500.0 + }, + "grid": { + "grid_current_l1": 12.5, + "grid_current_l2": 10.8, + "grid_current_l3": 11.2, + "grid_frequency": 50.0, + "grid_voltage_l1": 230.0, + "grid_voltage_l2": 229.5, + "grid_voltage_l3": 230.1 + }, + "input": { + "input_power_l1": 1000.0, + "input_power_l2": 950.0, + "input_power_l3": 980.0, + "input_power_total": 2930.0 + }, + "inverter": { + "inverter_charging_current_limit": 50, + "inverter_injection_power_limit": 5000.0, + "manager_inverter_state": "grid_consumption" + }, + "meter": { + "meter_power": 2000.0 + }, + "output": { + "output_current_l1": 15.0, + "output_current_l2": 14.5, + "output_current_l3": 15.2, + "output_frequency": 49.9, + "output_power_l1": 1100.0, + "output_power_l2": 1080.0, + "output_power_l3": 1120.0, + "output_power_total": 3300.0, + "output_voltage_l1": 231.0, + "output_voltage_l2": 229.8, + "output_voltage_l3": 230.2 + }, + "pv": { + "pv_consumed": 1500.0, + "pv_injected": 800.0, + "pv_power_1": 1200.0, + "pv_power_2": 1300.0, + "pv_power_total": 2500.0 + }, + "temp": { + "temp_air_temperature": 25.0, + "temp_component_temperature": 45.5 + }, + "monitoring": { + "monitoring_self_produced": 2600.0, + "monitoring_self_consumption": 85.0, + "monitoring_self_sufficiency": 90.0 + }, + "monitoring_minute": { + "monitoring_minute_building_consumption": 50.0, + "monitoring_minute_grid_consumption": 8.3, + "monitoring_minute_grid_injection": 11.7, + "monitoring_minute_grid_power_flow": -3.4, + "monitoring_minute_solar_production": 43.3 + }, + "timeline": { + "timeline_type_msg": "info_bat" + }, + "energy": { + "energy_pv": 12000.0, + "energy_grid_injected": 5000.0, + "energy_grid_consumed": 6000.0, + "energy_building_consumption": 15000.0, + "energy_battery_stored": 8000.0, + "energy_battery_consumed": 2000.0 + } +} diff --git a/tests/components/imeon_inverter/fixtures/sensor_data.json b/tests/components/imeon_inverter/fixtures/sensor_data.json deleted file mode 100644 index 566716fe3fa26..0000000000000 --- a/tests/components/imeon_inverter/fixtures/sensor_data.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "battery": { - "autonomy": 4.5, - "charge_time": 120, - "power": 2500.0, - "soc": 78.0, - "stored": 10.2 - }, - "grid": { - "current_l1": 12.5, - "current_l2": 10.8, - "current_l3": 11.2, - "frequency": 50.0, - "voltage_l1": 230.0, - "voltage_l2": 229.5, - "voltage_l3": 230.1 - }, - "input": { - "power_l1": 1000.0, - "power_l2": 950.0, - "power_l3": 980.0, - "power_total": 2930.0 - }, - "inverter": { - "charging_current_limit": 50, - "injection_power_limit": 5000.0 - }, - "meter": { - "power": 2000.0, - "power_protocol": 2018.0 - }, - "output": { - "current_l1": 15.0, - "current_l2": 14.5, - "current_l3": 15.2, - "frequency": 49.9, - "power_l1": 1100.0, - "power_l2": 1080.0, - "power_l3": 1120.0, - "power_total": 3300.0, - "voltage_l1": 231.0, - "voltage_l2": 229.8, - "voltage_l3": 230.2 - }, - "pv": { - "consumed": 1500.0, - "injected": 800.0, - "power_1": 1200.0, - "power_2": 1300.0, - "power_total": 2500.0 - }, - "temp": { - "air_temperature": 25.0, - "component_temperature": 45.5 - }, - "monitoring": { - "building_consumption": 3000.0, - "economy_factor": 0.8, - "grid_consumption": 500.0, - "grid_injection": 700.0, - "grid_power_flow": -200.0, - "self_consumption": 85.0, - "self_sufficiency": 90.0, - "solar_production": 2600.0 - }, - "monitoring_minute": { - "building_consumption": 50.0, - "grid_consumption": 8.3, - "grid_injection": 11.7, - "grid_power_flow": -3.4, - "solar_production": 43.3 - } -} diff --git a/tests/components/imeon_inverter/snapshots/test_sensor.ambr b/tests/components/imeon_inverter/snapshots/test_sensor.ambr index 84e691bc8de2d..673f561d54087 100644 --- a/tests/components/imeon_inverter/snapshots/test_sensor.ambr +++ b/tests/components/imeon_inverter/snapshots/test_sensor.ambr @@ -52,7 +52,63 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '25.0', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_battery_consumed-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_battery_consumed', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery consumed', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_consumed', + 'unique_id': '111111111111111_battery_consumed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_battery_consumed-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Battery consumed', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_battery_consumed', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_battery_power-entry] @@ -108,7 +164,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2500.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_battery_state_of_charge-entry] @@ -161,7 +217,67 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '78.0', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_battery_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'charging', + 'discharging', + 'charged', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_battery_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Battery status', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_status', + 'unique_id': '111111111111111_battery_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.imeon_inverter_battery_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Imeon inverter Battery status', + 'options': list([ + 'charging', + 'discharging', + 'charged', + ]), + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_battery_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_battery_stored-entry] @@ -217,7 +333,63 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '10.2', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_building_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_building_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Building consumption', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_minute_building_consumption', + 'unique_id': '111111111111111_monitoring_minute_building_consumption', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_building_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Building consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_building_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_charging_current_limit-entry] @@ -273,7 +445,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '50', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_component_temperature-entry] @@ -329,7 +501,63 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '45.5', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_grid_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_grid_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid consumption', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_minute_grid_consumption', + 'unique_id': '111111111111111_monitoring_minute_grid_consumption', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_grid_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Grid consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_grid_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_current_l1-entry] @@ -385,7 +613,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '12.5', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_current_l2-entry] @@ -441,7 +669,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '10.8', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_current_l3-entry] @@ -497,7 +725,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '11.2', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_grid_frequency-entry] @@ -553,10 +781,10 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '50.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_grid_voltage_l1-entry] +# name: test_sensors[sensor.imeon_inverter_grid_injection-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -571,7 +799,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_grid_voltage_l1', + 'entity_id': 'sensor.imeon_inverter_grid_injection', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -581,38 +809,38 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 0, + 'suggested_display_precision': 2, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Grid voltage L1', + 'original_name': 'Grid injection', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'grid_voltage_l1', - 'unique_id': '111111111111111_grid_voltage_l1', - 'unit_of_measurement': , + 'translation_key': 'monitoring_minute_grid_injection', + 'unique_id': '111111111111111_monitoring_minute_grid_injection', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_grid_voltage_l1-state] +# name: test_sensors[sensor.imeon_inverter_grid_injection-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Imeon inverter Grid voltage L1', + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Grid injection', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_grid_voltage_l1', + 'entity_id': 'sensor.imeon_inverter_grid_injection', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '230.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_grid_voltage_l2-entry] +# name: test_sensors[sensor.imeon_inverter_grid_power_flow-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -627,7 +855,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_grid_voltage_l2', + 'entity_id': 'sensor.imeon_inverter_grid_power_flow', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -637,38 +865,38 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 0, + 'suggested_display_precision': 2, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Grid voltage L2', + 'original_name': 'Grid power flow', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'grid_voltage_l2', - 'unique_id': '111111111111111_grid_voltage_l2', - 'unit_of_measurement': , + 'translation_key': 'monitoring_minute_grid_power_flow', + 'unique_id': '111111111111111_monitoring_minute_grid_power_flow', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_grid_voltage_l2-state] +# name: test_sensors[sensor.imeon_inverter_grid_power_flow-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'voltage', - 'friendly_name': 'Imeon inverter Grid voltage L2', + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Grid power flow', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_grid_voltage_l2', + 'entity_id': 'sensor.imeon_inverter_grid_power_flow', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '229.5', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_grid_voltage_l3-entry] +# name: test_sensors[sensor.imeon_inverter_grid_voltage_l1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -683,7 +911,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_grid_voltage_l3', + 'entity_id': 'sensor.imeon_inverter_grid_voltage_l1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -698,33 +926,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Grid voltage L3', + 'original_name': 'Grid voltage L1', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'grid_voltage_l3', - 'unique_id': '111111111111111_grid_voltage_l3', + 'translation_key': 'grid_voltage_l1', + 'unique_id': '111111111111111_grid_voltage_l1', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_grid_voltage_l3-state] +# name: test_sensors[sensor.imeon_inverter_grid_voltage_l1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'voltage', - 'friendly_name': 'Imeon inverter Grid voltage L3', + 'friendly_name': 'Imeon inverter Grid voltage L1', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_grid_voltage_l3', + 'entity_id': 'sensor.imeon_inverter_grid_voltage_l1', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '230.1', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_injection_power_limit-entry] +# name: test_sensors[sensor.imeon_inverter_grid_voltage_l2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -739,7 +967,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_injection_power_limit', + 'entity_id': 'sensor.imeon_inverter_grid_voltage_l2', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -752,35 +980,35 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Injection power limit', + 'original_name': 'Grid voltage L2', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'inverter_injection_power_limit', - 'unique_id': '111111111111111_inverter_injection_power_limit', - 'unit_of_measurement': , + 'translation_key': 'grid_voltage_l2', + 'unique_id': '111111111111111_grid_voltage_l2', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_injection_power_limit-state] +# name: test_sensors[sensor.imeon_inverter_grid_voltage_l2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Injection power limit', + 'device_class': 'voltage', + 'friendly_name': 'Imeon inverter Grid voltage L2', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_injection_power_limit', + 'entity_id': 'sensor.imeon_inverter_grid_voltage_l2', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '5000.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_input_power_l1-entry] +# name: test_sensors[sensor.imeon_inverter_grid_voltage_l3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -795,7 +1023,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_input_power_l1', + 'entity_id': 'sensor.imeon_inverter_grid_voltage_l3', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -808,35 +1036,35 @@ 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Input power L1', + 'original_name': 'Grid voltage L3', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'input_power_l1', - 'unique_id': '111111111111111_input_power_l1', - 'unit_of_measurement': , + 'translation_key': 'grid_voltage_l3', + 'unique_id': '111111111111111_grid_voltage_l3', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_input_power_l1-state] +# name: test_sensors[sensor.imeon_inverter_grid_voltage_l3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Input power L1', + 'device_class': 'voltage', + 'friendly_name': 'Imeon inverter Grid voltage L3', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_input_power_l1', + 'entity_id': 'sensor.imeon_inverter_grid_voltage_l3', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1000.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_input_power_l2-entry] +# name: test_sensors[sensor.imeon_inverter_injection_power_limit-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -851,7 +1079,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_input_power_l2', + 'entity_id': 'sensor.imeon_inverter_injection_power_limit', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -866,33 +1094,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Input power L2', + 'original_name': 'Injection power limit', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'input_power_l2', - 'unique_id': '111111111111111_input_power_l2', + 'translation_key': 'inverter_injection_power_limit', + 'unique_id': '111111111111111_inverter_injection_power_limit', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_input_power_l2-state] +# name: test_sensors[sensor.imeon_inverter_injection_power_limit-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', - 'friendly_name': 'Imeon inverter Input power L2', + 'friendly_name': 'Imeon inverter Injection power limit', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_input_power_l2', + 'entity_id': 'sensor.imeon_inverter_injection_power_limit', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '950.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_input_power_l3-entry] +# name: test_sensors[sensor.imeon_inverter_input_power_l1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -907,7 +1135,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_input_power_l3', + 'entity_id': 'sensor.imeon_inverter_input_power_l1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -922,33 +1150,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Input power L3', + 'original_name': 'Input power L1', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'input_power_l3', - 'unique_id': '111111111111111_input_power_l3', + 'translation_key': 'input_power_l1', + 'unique_id': '111111111111111_input_power_l1', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_input_power_l3-state] +# name: test_sensors[sensor.imeon_inverter_input_power_l1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', - 'friendly_name': 'Imeon inverter Input power L3', + 'friendly_name': 'Imeon inverter Input power L1', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_input_power_l3', + 'entity_id': 'sensor.imeon_inverter_input_power_l1', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '980.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_input_power_total-entry] +# name: test_sensors[sensor.imeon_inverter_input_power_l2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -963,7 +1191,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_input_power_total', + 'entity_id': 'sensor.imeon_inverter_input_power_l2', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -978,33 +1206,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Input power total', + 'original_name': 'Input power L2', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'input_power_total', - 'unique_id': '111111111111111_input_power_total', + 'translation_key': 'input_power_l2', + 'unique_id': '111111111111111_input_power_l2', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_input_power_total-state] +# name: test_sensors[sensor.imeon_inverter_input_power_l2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', - 'friendly_name': 'Imeon inverter Input power total', + 'friendly_name': 'Imeon inverter Input power L2', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_input_power_total', + 'entity_id': 'sensor.imeon_inverter_input_power_l2', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2930.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_meter_power-entry] +# name: test_sensors[sensor.imeon_inverter_input_power_l3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1019,7 +1247,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_meter_power', + 'entity_id': 'sensor.imeon_inverter_input_power_l3', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1034,33 +1262,33 @@ }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Meter power', + 'original_name': 'Input power L3', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'meter_power', - 'unique_id': '111111111111111_meter_power', + 'translation_key': 'input_power_l3', + 'unique_id': '111111111111111_input_power_l3', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_meter_power-state] +# name: test_sensors[sensor.imeon_inverter_input_power_l3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', - 'friendly_name': 'Imeon inverter Meter power', + 'friendly_name': 'Imeon inverter Input power L3', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_meter_power', + 'entity_id': 'sensor.imeon_inverter_input_power_l3', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2000.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_building_consumption_minute-entry] +# name: test_sensors[sensor.imeon_inverter_input_power_total-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1075,7 +1303,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_building_consumption_minute', + 'entity_id': 'sensor.imeon_inverter_input_power_total', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1085,44 +1313,49 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 2, + 'suggested_display_precision': 0, }), }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Monitoring building consumption (minute)', + 'original_name': 'Input power total', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'monitoring_minute_building_consumption', - 'unique_id': '111111111111111_monitoring_minute_building_consumption', + 'translation_key': 'input_power_total', + 'unique_id': '111111111111111_input_power_total', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_building_consumption_minute-state] +# name: test_sensors[sensor.imeon_inverter_input_power_total-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', - 'friendly_name': 'Imeon inverter Monitoring building consumption (minute)', + 'friendly_name': 'Imeon inverter Input power total', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_building_consumption_minute', + 'entity_id': 'sensor.imeon_inverter_input_power_total', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '50.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_consumption_minute-entry] +# name: test_sensors[sensor.imeon_inverter_inverter_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'options': list([ + 'unsynchronized', + 'grid_consumption', + 'grid_injection', + 'grid_synchronised_but_not_used', + ]), }), 'config_entry_id': , 'config_subentry_id': , @@ -1131,7 +1364,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_consumption_minute', + 'entity_id': 'sensor.imeon_inverter_inverter_state', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1140,39 +1373,40 @@ }), 'name': None, 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Monitoring grid consumption (minute)', + 'original_name': 'Inverter state', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'monitoring_minute_grid_consumption', - 'unique_id': '111111111111111_monitoring_minute_grid_consumption', - 'unit_of_measurement': , + 'translation_key': 'manager_inverter_state', + 'unique_id': '111111111111111_manager_inverter_state', + 'unit_of_measurement': None, }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_consumption_minute-state] +# name: test_sensors[sensor.imeon_inverter_inverter_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Monitoring grid consumption (minute)', - 'state_class': , - 'unit_of_measurement': , + 'device_class': 'enum', + 'friendly_name': 'Imeon inverter Inverter state', + 'options': list([ + 'unsynchronized', + 'grid_consumption', + 'grid_injection', + 'grid_synchronised_but_not_used', + ]), }), 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_consumption_minute', + 'entity_id': 'sensor.imeon_inverter_inverter_state', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '8.3', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_injection_minute-entry] +# name: test_sensors[sensor.imeon_inverter_meter_power-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1187,7 +1421,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_injection_minute', + 'entity_id': 'sensor.imeon_inverter_meter_power', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1197,38 +1431,38 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 2, + 'suggested_display_precision': 0, }), }), 'original_device_class': , 'original_icon': None, - 'original_name': 'Monitoring grid injection (minute)', + 'original_name': 'Meter power', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'monitoring_minute_grid_injection', - 'unique_id': '111111111111111_monitoring_minute_grid_injection', + 'translation_key': 'meter_power', + 'unique_id': '111111111111111_meter_power', 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_injection_minute-state] +# name: test_sensors[sensor.imeon_inverter_meter_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', - 'friendly_name': 'Imeon inverter Monitoring grid injection (minute)', + 'friendly_name': 'Imeon inverter Meter power', 'state_class': , 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_injection_minute', + 'entity_id': 'sensor.imeon_inverter_meter_power', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '11.7', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_power_flow_minute-entry] +# name: test_sensors[sensor.imeon_inverter_output_current_l1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1243,7 +1477,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_power_flow_minute', + 'entity_id': 'sensor.imeon_inverter_output_current_l1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1256,41 +1490,41 @@ 'suggested_display_precision': 2, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Monitoring grid power flow (minute)', + 'original_name': 'Output current L1', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'monitoring_minute_grid_power_flow', - 'unique_id': '111111111111111_monitoring_minute_grid_power_flow', - 'unit_of_measurement': , + 'translation_key': 'output_current_l1', + 'unique_id': '111111111111111_output_current_l1', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_grid_power_flow_minute-state] +# name: test_sensors[sensor.imeon_inverter_output_current_l1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Monitoring grid power flow (minute)', + 'device_class': 'current', + 'friendly_name': 'Imeon inverter Output current L1', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_grid_power_flow_minute', + 'entity_id': 'sensor.imeon_inverter_output_current_l1', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '-3.4', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_self_consumption-entry] +# name: test_sensors[sensor.imeon_inverter_output_current_l2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -1299,7 +1533,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_self_consumption', + 'entity_id': 'sensor.imeon_inverter_output_current_l2', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1312,40 +1546,41 @@ 'suggested_display_precision': 2, }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Monitoring self-consumption', + 'original_name': 'Output current L2', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'monitoring_self_consumption', - 'unique_id': '111111111111111_monitoring_self_consumption', - 'unit_of_measurement': '%', + 'translation_key': 'output_current_l2', + 'unique_id': '111111111111111_output_current_l2', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_self_consumption-state] +# name: test_sensors[sensor.imeon_inverter_output_current_l2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Imeon inverter Monitoring self-consumption', - 'state_class': , - 'unit_of_measurement': '%', + 'device_class': 'current', + 'friendly_name': 'Imeon inverter Output current L2', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_self_consumption', + 'entity_id': 'sensor.imeon_inverter_output_current_l2', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '85.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_self_sufficiency-entry] +# name: test_sensors[sensor.imeon_inverter_output_current_l3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), 'area_id': None, 'capabilities': dict({ - 'state_class': , + 'state_class': , }), 'config_entry_id': , 'config_subentry_id': , @@ -1354,7 +1589,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_self_sufficiency', + 'entity_id': 'sensor.imeon_inverter_output_current_l3', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1367,34 +1602,35 @@ 'suggested_display_precision': 2, }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, - 'original_name': 'Monitoring self-sufficiency', + 'original_name': 'Output current L3', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'monitoring_self_sufficiency', - 'unique_id': '111111111111111_monitoring_self_sufficiency', - 'unit_of_measurement': '%', + 'translation_key': 'output_current_l3', + 'unique_id': '111111111111111_output_current_l3', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_self_sufficiency-state] +# name: test_sensors[sensor.imeon_inverter_output_current_l3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Imeon inverter Monitoring self-sufficiency', - 'state_class': , - 'unit_of_measurement': '%', + 'device_class': 'current', + 'friendly_name': 'Imeon inverter Output current L3', + 'state_class': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_self_sufficiency', + 'entity_id': 'sensor.imeon_inverter_output_current_l3', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '90.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_solar_production_minute-entry] +# name: test_sensors[sensor.imeon_inverter_output_frequency-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1409,7 +1645,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_monitoring_solar_production_minute', + 'entity_id': 'sensor.imeon_inverter_output_frequency', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1419,38 +1655,38 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 2, + 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Monitoring solar production (minute)', + 'original_name': 'Output frequency', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'monitoring_minute_solar_production', - 'unique_id': '111111111111111_monitoring_minute_solar_production', - 'unit_of_measurement': , + 'translation_key': 'output_frequency', + 'unique_id': '111111111111111_output_frequency', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_monitoring_solar_production_minute-state] +# name: test_sensors[sensor.imeon_inverter_output_frequency-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Monitoring solar production (minute)', + 'device_class': 'frequency', + 'friendly_name': 'Imeon inverter Output frequency', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_monitoring_solar_production_minute', + 'entity_id': 'sensor.imeon_inverter_output_frequency', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '43.3', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_output_current_l1-entry] +# name: test_sensors[sensor.imeon_inverter_output_power_l1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1465,7 +1701,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_output_current_l1', + 'entity_id': 'sensor.imeon_inverter_output_power_l1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1475,38 +1711,38 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 2, + 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Output current L1', + 'original_name': 'Output power L1', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'output_current_l1', - 'unique_id': '111111111111111_output_current_l1', - 'unit_of_measurement': , + 'translation_key': 'output_power_l1', + 'unique_id': '111111111111111_output_power_l1', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_output_current_l1-state] +# name: test_sensors[sensor.imeon_inverter_output_power_l1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Imeon inverter Output current L1', + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Output power L1', 'state_class': , - 'unit_of_measurement': , + 'unit_of_measurement': , }), 'context': , - 'entity_id': 'sensor.imeon_inverter_output_current_l1', + 'entity_id': 'sensor.imeon_inverter_output_power_l1', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '15.0', + 'state': 'unknown', }) # --- -# name: test_sensors[sensor.imeon_inverter_output_current_l2-entry] +# name: test_sensors[sensor.imeon_inverter_output_power_l2-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -1521,7 +1757,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_output_current_l2', + 'entity_id': 'sensor.imeon_inverter_output_power_l2', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -1531,246 +1767,22 @@ 'name': None, 'options': dict({ 'sensor': dict({ - 'suggested_display_precision': 2, + 'suggested_display_precision': 0, }), }), - 'original_device_class': , + 'original_device_class': , 'original_icon': None, - 'original_name': 'Output current L2', + 'original_name': 'Output power L2', 'platform': 'imeon_inverter', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'output_current_l2', - 'unique_id': '111111111111111_output_current_l2', - 'unit_of_measurement': , + 'translation_key': 'output_power_l2', + 'unique_id': '111111111111111_output_power_l2', + 'unit_of_measurement': , }) # --- -# name: test_sensors[sensor.imeon_inverter_output_current_l2-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Imeon inverter Output current L2', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_output_current_l2', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '14.5', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_output_current_l3-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_output_current_l3', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 2, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Output current L3', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'output_current_l3', - 'unique_id': '111111111111111_output_current_l3', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.imeon_inverter_output_current_l3-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'current', - 'friendly_name': 'Imeon inverter Output current L3', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_output_current_l3', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '15.2', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_output_frequency-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_output_frequency', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Output frequency', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'output_frequency', - 'unique_id': '111111111111111_output_frequency', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.imeon_inverter_output_frequency-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'frequency', - 'friendly_name': 'Imeon inverter Output frequency', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_output_frequency', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '49.9', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_output_power_l1-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_output_power_l1', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Output power L1', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'output_power_l1', - 'unique_id': '111111111111111_output_power_l1', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.imeon_inverter_output_power_l1-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'power', - 'friendly_name': 'Imeon inverter Output power L1', - 'state_class': , - 'unit_of_measurement': , - }), - 'context': , - 'entity_id': 'sensor.imeon_inverter_output_power_l1', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1100.0', - }) -# --- -# name: test_sensors[sensor.imeon_inverter_output_power_l2-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'state_class': , - }), - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': , - 'entity_id': 'sensor.imeon_inverter_output_power_l2', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Output power L2', - 'platform': 'imeon_inverter', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'output_power_l2', - 'unique_id': '111111111111111_output_power_l2', - 'unit_of_measurement': , - }) -# --- -# name: test_sensors[sensor.imeon_inverter_output_power_l2-state] +# name: test_sensors[sensor.imeon_inverter_output_power_l2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'power', @@ -1783,7 +1795,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1080.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_power_l3-entry] @@ -1839,7 +1851,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1120.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_power_total-entry] @@ -1895,7 +1907,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '3300.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_voltage_l1-entry] @@ -1951,7 +1963,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '231.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_voltage_l2-entry] @@ -2007,7 +2019,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '229.8', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_output_voltage_l3-entry] @@ -2063,7 +2075,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '230.2', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_pv_consumed-entry] @@ -2119,7 +2131,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1500.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_pv_injected-entry] @@ -2175,7 +2187,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '800.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_pv_power_1-entry] @@ -2231,7 +2243,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1200.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_pv_power_2-entry] @@ -2287,7 +2299,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '1300.0', + 'state': 'unknown', }) # --- # name: test_sensors[sensor.imeon_inverter_pv_power_total-entry] @@ -2343,6 +2355,590 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2500.0', + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_self_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_self_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Self-consumption', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_self_consumption', + 'unique_id': '111111111111111_monitoring_self_consumption', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_self_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Imeon inverter Self-consumption', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_self_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_self_sufficiency-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_self_sufficiency', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Self-sufficiency', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_self_sufficiency', + 'unique_id': '111111111111111_monitoring_self_sufficiency', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_self_sufficiency-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Imeon inverter Self-sufficiency', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_self_sufficiency', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_solar_production-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_solar_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Solar production', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'monitoring_minute_solar_production', + 'unique_id': '111111111111111_monitoring_minute_solar_production', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_solar_production-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Imeon inverter Solar production', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_solar_production', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_timeline_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'com_lost', + 'warning_grid', + 'warning_pv', + 'warning_bat', + 'error_ond', + 'error_soft', + 'error_pv', + 'error_grid', + 'error_bat', + 'good_1', + 'info_soft', + 'info_ond', + 'info_bat', + 'info_smartlo', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_timeline_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Timeline status', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'timeline_type_msg', + 'unique_id': '111111111111111_timeline_type_msg', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensors[sensor.imeon_inverter_timeline_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Imeon inverter Timeline status', + 'options': list([ + 'com_lost', + 'warning_grid', + 'warning_pv', + 'warning_bat', + 'error_ond', + 'error_soft', + 'error_pv', + 'error_grid', + 'error_bat', + 'good_1', + 'info_soft', + 'info_ond', + 'info_bat', + 'info_smartlo', + ]), + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_timeline_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_battery_consumed_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_battery_consumed_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today battery-consumed energy', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_battery_consumed', + 'unique_id': '111111111111111_energy_battery_consumed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_battery_consumed_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today battery-consumed energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_battery_consumed_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_battery_stored_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_battery_stored_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today battery-stored energy', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_battery_stored', + 'unique_id': '111111111111111_energy_battery_stored', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_battery_stored_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today battery-stored energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_battery_stored_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_building_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_building_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today building consumption', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_building_consumption', + 'unique_id': '111111111111111_energy_building_consumption', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_building_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today building consumption', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_building_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_grid_consumed_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_grid_consumed_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today grid-consumed energy', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_grid_consumed', + 'unique_id': '111111111111111_energy_grid_consumed', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_grid_consumed_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today grid-consumed energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_grid_consumed_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_grid_injected_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_grid_injected_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today grid-injected energy', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_grid_injected', + 'unique_id': '111111111111111_energy_grid_injected', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_grid_injected_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today grid-injected energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_grid_injected_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_pv_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.imeon_inverter_today_pv_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Today PV energy', + 'platform': 'imeon_inverter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_pv', + 'unique_id': '111111111111111_energy_pv', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.imeon_inverter_today_pv_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Imeon inverter Today PV energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.imeon_inverter_today_pv_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', }) # --- diff --git a/tests/components/imeon_inverter/test_sensor.py b/tests/components/imeon_inverter/test_sensor.py index 194864a67a2e9..ec50594f6ba98 100644 --- a/tests/components/imeon_inverter/test_sensor.py +++ b/tests/components/imeon_inverter/test_sensor.py @@ -1,16 +1,20 @@ """Test the Imeon Inverter sensors.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch +from aiohttp import ClientError +from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.const import Platform +from homeassistant.components.imeon_inverter.coordinator import INTERVAL +from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform async def test_sensors( @@ -24,3 +28,51 @@ async def test_sensors( await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "exception", + [ + TimeoutError, + ClientError, + ValueError, + ], +) +@pytest.mark.asyncio +async def test_sensor_unavailable_on_update_error( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_imeon_inverter: MagicMock, + freezer: FrozenDateTimeFactory, + exception: Exception, +) -> None: + """Test that sensor becomes unavailable when update raises an error.""" + entity_id = "sensor.imeon_inverter_battery_power" + + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNAVAILABLE + + mock_imeon_inverter.update.side_effect = exception + + freezer.tick(INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state == STATE_UNAVAILABLE + + mock_imeon_inverter.update.side_effect = None + + freezer.tick(INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert state.state != STATE_UNAVAILABLE From 319e37384fce36648f0f92b41abef794b11a1804 Mon Sep 17 00:00:00 2001 From: Alexandre CUER Date: Tue, 19 Aug 2025 14:32:13 +0200 Subject: [PATCH 08/23] Migrate Emoncms_history to external async library (#149824) --- CODEOWNERS | 2 + .../components/emoncms_history/__init__.py | 103 +++++++-------- .../components/emoncms_history/manifest.json | 5 +- requirements_all.txt | 1 + requirements_test_all.txt | 1 + tests/components/emoncms_history/__init__.py | 1 + tests/components/emoncms_history/test_init.py | 125 ++++++++++++++++++ 7 files changed, 180 insertions(+), 58 deletions(-) create mode 100644 tests/components/emoncms_history/__init__.py create mode 100644 tests/components/emoncms_history/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index c372cb70371b6..7da06479b9213 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -422,6 +422,8 @@ build.json @home-assistant/supervisor /homeassistant/components/emby/ @mezz64 /homeassistant/components/emoncms/ @borpin @alexandrecuer /tests/components/emoncms/ @borpin @alexandrecuer +/homeassistant/components/emoncms_history/ @alexandrecuer +/tests/components/emoncms_history/ @alexandrecuer /homeassistant/components/emonitor/ @bdraco /tests/components/emonitor/ @bdraco /homeassistant/components/emulated_hue/ @bdraco @Tho85 diff --git a/homeassistant/components/emoncms_history/__init__.py b/homeassistant/components/emoncms_history/__init__.py index 2ab00d6ca4289..5394a797272bd 100644 --- a/homeassistant/components/emoncms_history/__init__.py +++ b/homeassistant/components/emoncms_history/__init__.py @@ -1,10 +1,11 @@ """Support for sending data to Emoncms.""" -from datetime import timedelta -from http import HTTPStatus +from datetime import datetime, timedelta +from functools import partial import logging -import requests +import aiohttp +from pyemoncms import EmoncmsClient import voluptuous as vol from homeassistant.const import ( @@ -17,9 +18,9 @@ ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, state as state_helper -from homeassistant.helpers.event import track_point_in_time +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -42,61 +43,51 @@ ) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up the Emoncms history component.""" - conf = config[DOMAIN] - whitelist = conf.get(CONF_WHITELIST) - - def send_data(url, apikey, node, payload): - """Send payload data to Emoncms.""" +async def async_send_to_emoncms( + hass: HomeAssistant, + emoncms_client: EmoncmsClient, + whitelist: list[str], + node: str | int, + _: datetime, +) -> None: + """Send data to Emoncms.""" + payload_dict = {} + + for entity_id in whitelist: + state = hass.states.get(entity_id) + if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE): + continue try: - fullurl = f"{url}/input/post.json" - data = {"apikey": apikey, "data": payload} - parameters = {"node": node} - req = requests.post( - fullurl, params=parameters, data=data, allow_redirects=True, timeout=5 - ) - - except requests.exceptions.RequestException: - _LOGGER.error("Error saving data '%s' to '%s'", payload, fullurl) + payload_dict[entity_id] = state_helper.state_as_number(state) + except ValueError: + continue + if payload_dict: + try: + await emoncms_client.async_input_post(data=payload_dict, node=node) + except (aiohttp.ClientError, TimeoutError) as err: + _LOGGER.warning("Network error when sending data to Emoncms: %s", err) + except ValueError as err: + _LOGGER.warning("Value error when preparing data for Emoncms: %s", err) else: - if req.status_code != HTTPStatus.OK: - _LOGGER.error( - "Error saving data %s to %s (http status code = %d)", - payload, - fullurl, - req.status_code, - ) - - def update_emoncms(time): - """Send whitelisted entities states regularly to Emoncms.""" - payload_dict = {} - - for entity_id in whitelist: - state = hass.states.get(entity_id) + _LOGGER.debug("Sent data to Emoncms: %s", payload_dict) - if state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE): - continue - try: - payload_dict[entity_id] = state_helper.state_as_number(state) - except ValueError: - continue - - if payload_dict: - payload = ",".join(f"{key}:{val}" for key, val in payload_dict.items()) - - send_data( - conf.get(CONF_URL), - conf.get(CONF_API_KEY), - str(conf.get(CONF_INPUTNODE)), - f"{{{payload}}}", - ) - - track_point_in_time( - hass, update_emoncms, time + timedelta(seconds=conf.get(CONF_SCAN_INTERVAL)) - ) +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Emoncms history component.""" + conf = config[DOMAIN] + whitelist = conf.get(CONF_WHITELIST) + input_node = str(conf.get(CONF_INPUTNODE)) + + emoncms_client = EmoncmsClient( + url=conf.get(CONF_URL), + api_key=conf.get(CONF_API_KEY), + session=async_get_clientsession(hass), + ) + async_track_time_interval( + hass, + partial(async_send_to_emoncms, hass, emoncms_client, whitelist, input_node), + timedelta(seconds=conf.get(CONF_SCAN_INTERVAL)), + ) - update_emoncms(dt_util.utcnow()) return True diff --git a/homeassistant/components/emoncms_history/manifest.json b/homeassistant/components/emoncms_history/manifest.json index e73f76f752862..3c8c445b766ea 100644 --- a/homeassistant/components/emoncms_history/manifest.json +++ b/homeassistant/components/emoncms_history/manifest.json @@ -1,8 +1,9 @@ { "domain": "emoncms_history", "name": "Emoncms History", - "codeowners": [], + "codeowners": ["@alexandrecuer"], "documentation": "https://www.home-assistant.io/integrations/emoncms_history", "iot_class": "local_polling", - "quality_scale": "legacy" + "quality_scale": "legacy", + "requirements": ["pyemoncms==0.1.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 17052c94bf139..d3e86ebd3421e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1957,6 +1957,7 @@ pyefergy==22.5.0 pyegps==0.2.5 # homeassistant.components.emoncms +# homeassistant.components.emoncms_history pyemoncms==0.1.2 # homeassistant.components.enphase_envoy diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 669695558d9dd..624b9b5f6ba8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1632,6 +1632,7 @@ pyefergy==22.5.0 pyegps==0.2.5 # homeassistant.components.emoncms +# homeassistant.components.emoncms_history pyemoncms==0.1.2 # homeassistant.components.enphase_envoy diff --git a/tests/components/emoncms_history/__init__.py b/tests/components/emoncms_history/__init__.py new file mode 100644 index 0000000000000..0c60d27655b2a --- /dev/null +++ b/tests/components/emoncms_history/__init__.py @@ -0,0 +1 @@ +"""Tests for emoncms_history component.""" diff --git a/tests/components/emoncms_history/test_init.py b/tests/components/emoncms_history/test_init.py new file mode 100644 index 0000000000000..c62252750b540 --- /dev/null +++ b/tests/components/emoncms_history/test_init.py @@ -0,0 +1,125 @@ +"""The tests for the emoncms_history init.""" + +from collections.abc import AsyncGenerator +from datetime import timedelta +from unittest.mock import AsyncMock, patch + +import aiohttp +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.const import CONF_API_KEY, CONF_URL, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + + +async def test_setup_valid_config(hass: HomeAssistant) -> None: + """Test setting up the emoncms_history component with valid configuration.""" + config = { + "emoncms_history": { + CONF_API_KEY: "dummy", + CONF_URL: "https://emoncms.example", + "inputnode": 42, + "whitelist": ["sensor.temp"], + } + } + # Simulate a sensor + hass.states.async_set("sensor.temp", "23.4", {"unit_of_measurement": "°C"}) + await hass.async_block_till_done() + + assert await async_setup_component(hass, "emoncms_history", config) + await hass.async_block_till_done() + + +async def test_setup_missing_config(hass: HomeAssistant) -> None: + """Test setting up the emoncms_history component with missing configuration.""" + config = {"emoncms_history": {"api_key": "dummy"}} + success = await async_setup_component(hass, "emoncms_history", config) + assert not success + + +@pytest.fixture +async def emoncms_client() -> AsyncGenerator[AsyncMock]: + """Mock pyemoncms client with successful responses.""" + with patch( + "homeassistant.components.emoncms_history.EmoncmsClient", autospec=True + ) as mock_client: + client = mock_client.return_value + client.async_input_post.return_value = '{"success": true}' + yield client + + +async def test_emoncms_send_data( + hass: HomeAssistant, + emoncms_client: AsyncMock, + caplog: pytest.LogCaptureFixture, + freezer: FrozenDateTimeFactory, +) -> None: + """Test sending data to Emoncms with and without success.""" + + config = { + "emoncms_history": { + "api_key": "dummy", + "url": "http://fake-url", + "inputnode": 42, + "whitelist": ["sensor.temp"], + } + } + + assert await async_setup_component(hass, "emoncms_history", config) + await hass.async_block_till_done() + + for state in None, "", STATE_UNAVAILABLE, STATE_UNKNOWN: + hass.states.async_set("sensor.temp", state, {"unit_of_measurement": "°C"}) + await hass.async_block_till_done() + + freezer.tick(timedelta(seconds=60)) + await hass.async_block_till_done() + + assert emoncms_client.async_input_post.call_args is None + + hass.states.async_set("sensor.temp", "not_a_number", {"unit_of_measurement": "°C"}) + await hass.async_block_till_done() + + freezer.tick(timedelta(seconds=60)) + await hass.async_block_till_done() + + emoncms_client.async_input_post.assert_not_called() + + hass.states.async_set("sensor.temp", "23.4", {"unit_of_measurement": "°C"}) + await hass.async_block_till_done() + + freezer.tick(timedelta(seconds=60)) + await hass.async_block_till_done() + + emoncms_client.async_input_post.assert_called_once() + assert emoncms_client.async_input_post.return_value == '{"success": true}' + + _, kwargs = emoncms_client.async_input_post.call_args + assert kwargs["data"] == {"sensor.temp": 23.4} + assert kwargs["node"] == "42" + + emoncms_client.async_input_post.side_effect = aiohttp.ClientError( + "Connection refused" + ) + await hass.async_block_till_done() + + freezer.tick(timedelta(seconds=60)) + await hass.async_block_till_done() + + assert any( + "Network error when sending data to Emoncms" in message + for message in caplog.text.splitlines() + ) + + emoncms_client.async_input_post.side_effect = ValueError("Invalid value format") + + await hass.async_block_till_done() + + freezer.tick(timedelta(seconds=60)) + await hass.async_block_till_done() + + assert any( + "Value error when preparing data for Emoncms" in message + for message in caplog.text.splitlines() + ) From c46618cbd136e5df14b3657b987faee3384baa77 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:37:56 +0200 Subject: [PATCH 09/23] Bump renault-api to 0.4.0 (#150624) --- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 2861c52c24ac0..9fe01c5b95296 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "silver", - "requirements": ["renault-api==0.3.1"] + "requirements": ["renault-api==0.4.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d3e86ebd3421e..b572a7ef178db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2655,7 +2655,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.1 +renault-api==0.4.0 # homeassistant.components.renson renson-endura-delta==1.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 624b9b5f6ba8b..7ff86f86db16b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2201,7 +2201,7 @@ refoss-ha==1.2.5 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.3.1 +renault-api==0.4.0 # homeassistant.components.renson renson-endura-delta==1.7.2 From e8409e7c42cd47798e713e72e10f2fe978c0bf4b Mon Sep 17 00:00:00 2001 From: markhannon Date: Tue, 19 Aug 2025 22:40:12 +1000 Subject: [PATCH 10/23] Bump to zcc-helper==3.6 (#150608) --- homeassistant/components/zimi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zimi/manifest.json b/homeassistant/components/zimi/manifest.json index 3e019d2f053d5..58a56c978305a 100644 --- a/homeassistant/components/zimi/manifest.json +++ b/homeassistant/components/zimi/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/zimi", "iot_class": "local_push", "quality_scale": "bronze", - "requirements": ["zcc-helper==3.5.2"] + "requirements": ["zcc-helper==3.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index b572a7ef178db..af2db386a6493 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3189,7 +3189,7 @@ zabbix-utils==2.0.2 zamg==0.3.6 # homeassistant.components.zimi -zcc-helper==3.5.2 +zcc-helper==3.6 # homeassistant.components.zeroconf zeroconf==0.147.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7ff86f86db16b..8735fbd403987 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2633,7 +2633,7 @@ yt-dlp[default]==2025.08.11 zamg==0.3.6 # homeassistant.components.zimi -zcc-helper==3.5.2 +zcc-helper==3.6 # homeassistant.components.zeroconf zeroconf==0.147.0 From a08be4fcb69dfa3a9e4816ef905b7bab6dace2ea Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Tue, 19 Aug 2025 15:01:57 +0200 Subject: [PATCH 11/23] Add event entity to Togrill (#150812) --- homeassistant/components/togrill/__init__.py | 2 +- .../components/togrill/coordinator.py | 20 + homeassistant/components/togrill/event.py | 59 ++ homeassistant/components/togrill/strings.json | 14 + .../togrill/snapshots/test_event.ambr | 721 ++++++++++++++++++ tests/components/togrill/test_event.py | 69 ++ 6 files changed, 884 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/togrill/event.py create mode 100644 tests/components/togrill/snapshots/test_event.ambr create mode 100644 tests/components/togrill/test_event.py diff --git a/homeassistant/components/togrill/__init__.py b/homeassistant/components/togrill/__init__.py index fcacc851dd93c..696b7395f1e26 100644 --- a/homeassistant/components/togrill/__init__.py +++ b/homeassistant/components/togrill/__init__.py @@ -8,7 +8,7 @@ from .coordinator import DeviceNotFound, ToGrillConfigEntry, ToGrillCoordinator -_PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.NUMBER] +_PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.NUMBER] async def async_setup_entry(hass: HomeAssistant, entry: ToGrillConfigEntry) -> bool: diff --git a/homeassistant/components/togrill/coordinator.py b/homeassistant/components/togrill/coordinator.py index b2f20963fc8bc..75964067de789 100644 --- a/homeassistant/components/togrill/coordinator.py +++ b/homeassistant/components/togrill/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable from datetime import timedelta import logging from typing import TypeVar @@ -78,6 +79,7 @@ def __init__( self.device_info = DeviceInfo( connections={(CONNECTION_BLUETOOTH, self.address)} ) + self._packet_listeners: list[Callable[[Packet], None]] = [] config_entry.async_on_unload( async_register_callback( @@ -88,6 +90,23 @@ def __init__( ) ) + @callback + def async_add_packet_listener( + self, packet_callback: Callable[[Packet], None] + ) -> Callable[[], None]: + """Add a listener for a given packet type.""" + + def _unregister(): + self._packet_listeners.remove(packet_callback) + + self._packet_listeners.append(packet_callback) + return _unregister + + def async_update_packet_listeners(self, packet: Packet): + """Update all packet listeners.""" + for listener in self._packet_listeners: + listener(packet) + async def _connect_and_update_registry(self) -> Client: """Update device registry data.""" device = bluetooth.async_ble_device_from_address( @@ -151,6 +170,7 @@ def get_packet( def _notify_callback(self, packet: Packet): probe = getattr(packet, "probe", None) self.data[(packet.type, probe)] = packet + self.async_update_packet_listeners(packet) self.async_update_listeners() async def _async_update_data(self) -> dict[tuple[int, int | None], Packet]: diff --git a/homeassistant/components/togrill/event.py b/homeassistant/components/togrill/event.py new file mode 100644 index 0000000000000..eb0cc50208080 --- /dev/null +++ b/homeassistant/components/togrill/event.py @@ -0,0 +1,59 @@ +"""Support for event entities.""" + +from __future__ import annotations + +from togrill_bluetooth.packets import Packet, PacketA5Notify + +from homeassistant.components.event import EventEntity +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.util import slugify + +from . import ToGrillConfigEntry +from .const import CONF_PROBE_COUNT +from .coordinator import ToGrillCoordinator +from .entity import ToGrillEntity + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ToGrillConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up event platform.""" + async_add_entities( + ToGrillEventEntity(config_entry.runtime_data, probe_number=probe_number) + for probe_number in range(1, config_entry.data[CONF_PROBE_COUNT] + 1) + ) + + +class ToGrillEventEntity(ToGrillEntity, EventEntity): + """Representation of a Hue Event entity from a button resource.""" + + def __init__(self, coordinator: ToGrillCoordinator, probe_number: int) -> None: + """Initialize the entity.""" + super().__init__(coordinator=coordinator) + + self._attr_translation_key = "event" + self._attr_translation_placeholders = {"probe_number": f"{probe_number}"} + self._attr_unique_id = f"{coordinator.address}_{probe_number}" + self._probe_number = probe_number + + self._attr_event_types: list[str] = [ + slugify(event.name) for event in PacketA5Notify.Message + ] + + self.async_on_remove(coordinator.async_add_packet_listener(self._handle_event)) + + @callback + def _handle_event(self, packet: Packet) -> None: + if not isinstance(packet, PacketA5Notify): + return + + try: + message = PacketA5Notify.Message(packet.message) + except ValueError: + return + self._trigger_event(slugify(message.name)) diff --git a/homeassistant/components/togrill/strings.json b/homeassistant/components/togrill/strings.json index a49b6613d3caf..309ac65f54cb4 100644 --- a/homeassistant/components/togrill/strings.json +++ b/homeassistant/components/togrill/strings.json @@ -46,6 +46,20 @@ "alarm_interval": { "name": "Alarm interval" } + }, + "event": { + "event": { + "name": "Probe {probe_number}", + "state_attributes": { + "event_type": { + "state": { + "probe_acknowledge": "Alarm acknowledged", + "probe_alarm": "Alarm triggered", + "probe_disconnected": "Probe disconnected" + } + } + } + } } } } diff --git a/tests/components/togrill/snapshots/test_event.ambr b/tests/components/togrill/snapshots/test_event.ambr new file mode 100644 index 0000000000000..18b175e7513e1 --- /dev/null +++ b/tests/components/togrill/snapshots/test_event.ambr @@ -0,0 +1,721 @@ +# serializer version: 1 +# name: test_events[0][event.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[0][event.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': 'probe_acknowledge', + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 1', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-10-21T00:00:00.000+00:00', + }) +# --- +# name: test_events[0][event.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[0][event.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': 'probe_acknowledge', + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 2', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-10-21T00:00:00.000+00:00', + }) +# --- +# name: test_events[5][event.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[5][event.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': 'probe_alarm', + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 1', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-10-21T00:00:00.000+00:00', + }) +# --- +# name: test_events[5][event.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[5][event.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': 'probe_alarm', + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 2', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-10-21T00:00:00.000+00:00', + }) +# --- +# name: test_events[6][event.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[6][event.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': 'probe_disconnected', + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 1', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-10-21T00:00:00.000+00:00', + }) +# --- +# name: test_events[6][event.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_events[6][event.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': 'probe_disconnected', + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 2', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2023-10-21T00:00:00.000+00:00', + }) +# --- +# name: test_setup[no_data][event.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[no_data][event.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 1', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[no_data][event.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[no_data][event.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 2', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[non_event_packet][event.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[non_event_packet][event.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 1', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[non_event_packet][event.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[non_event_packet][event.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 2', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[non_known_message][event.pro_05_probe_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 1', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[non_known_message][event.pro_05_probe_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 1', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_setup[non_known_message][event.pro_05_probe_2-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'event', + 'entity_category': None, + 'entity_id': 'event.pro_05_probe_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Probe 2', + 'platform': 'togrill', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'event', + 'unique_id': '00000000-0000-0000-0000-000000000001_2', + 'unit_of_measurement': None, + }) +# --- +# name: test_setup[non_known_message][event.pro_05_probe_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'event_type': None, + 'event_types': list([ + 'probe_acknowledge', + 'probe_alarm', + 'probe_disconnected', + ]), + 'friendly_name': 'Pro-05 Probe 2', + }), + 'context': , + 'entity_id': 'event.pro_05_probe_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/togrill/test_event.py b/tests/components/togrill/test_event.py new file mode 100644 index 0000000000000..932e6e93433e9 --- /dev/null +++ b/tests/components/togrill/test_event.py @@ -0,0 +1,69 @@ +"""Test events for ToGrill integration.""" + +from unittest.mock import Mock + +import pytest +from syrupy.assertion import SnapshotAssertion +from togrill_bluetooth.packets import PacketA1Notify, PacketA5Notify + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import TOGRILL_SERVICE_INFO, setup_entry + +from tests.common import MockConfigEntry, snapshot_platform +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.mark.freeze_time("2023-10-21") +@pytest.mark.parametrize( + "packets", + [ + pytest.param([], id="no_data"), + pytest.param([PacketA1Notify([10, None])], id="non_event_packet"), + pytest.param([PacketA5Notify(probe=1, message=99)], id="non_known_message"), + ], +) +async def test_setup( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + packets, +) -> None: + """Test standard events.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.EVENT]) + + for packet in packets: + mock_client.mocked_notify(packet) + + await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id) + + +@pytest.mark.freeze_time("2023-10-21") +@pytest.mark.parametrize( + "message", + list(PacketA5Notify.Message), +) +async def test_events( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_entry: MockConfigEntry, + mock_client: Mock, + message, +) -> None: + """Test all possible events.""" + + inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO) + + await setup_entry(hass, mock_entry, [Platform.EVENT]) + + mock_client.mocked_notify(PacketA5Notify(probe=1, message=message)) + + await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id) From 785c9ebc3b4e56fd6866477eeb3dc13b578b39a5 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 19 Aug 2025 15:03:05 +0200 Subject: [PATCH 12/23] Modbus: Retry primary connect. (#150853) --- homeassistant/components/modbus/modbus.py | 26 +++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 7343ffd17874f..5758762305871 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -71,6 +71,7 @@ DATA_MODBUS_HUBS: HassKey[dict[str, ModbusHub]] = HassKey(DOMAIN) +PRIMARY_RECONNECT_DELAY = 60 ConfEntry = namedtuple("ConfEntry", "call_type attr func_name value_attr_name") # noqa: PYI024 RunEntry = namedtuple("RunEntry", "attr func value_attr_name") # noqa: PYI024 @@ -311,18 +312,21 @@ def _log_error(self, text: str) -> None: async def async_pb_connect(self) -> None: """Connect to device, async.""" - async with self._lock: - try: - await self._client.connect() # type: ignore[union-attr] - except ModbusException as exception_error: - self._log_error( - f"{self.name} connect failed, please check your configuration ({exception_error!s})" - ) - return - message = f"modbus {self.name} communication open" - _LOGGER.info(message) + while True: + async with self._lock: + try: + if await self._client.connect(): # type: ignore[union-attr] + _LOGGER.info(f"modbus {self.name} communication open") + break + except ModbusException as exception_error: + self._log_error( + f"{self.name} connect failed, please check your configuration ({exception_error!s})" + ) + _LOGGER.info( + f"modbus {self.name} connect NOT a success ! retrying in {PRIMARY_RECONNECT_DELAY} seconds" + ) + await asyncio.sleep(PRIMARY_RECONNECT_DELAY) - # Start counting down to allow modbus requests. if self.config_delay: await asyncio.sleep(self.config_delay) self.config_delay = 0 From 89abe65e1d6cf61e3a14c481b787adc917a6cf2c Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Tue, 19 Aug 2025 22:02:18 +0800 Subject: [PATCH 13/23] Add air purifier for switchbot cloud integration (#147001) --- .../components/switchbot_cloud/__init__.py | 5 + .../components/switchbot_cloud/const.py | 16 +- .../components/switchbot_cloud/fan.py | 93 +++++++++- .../components/switchbot_cloud/icons.json | 22 +++ .../components/switchbot_cloud/strings.json | 16 ++ tests/components/switchbot_cloud/__init__.py | 19 ++ .../fixtures/air_purifier_status.json | 8 + .../switchbot_cloud/snapshots/test_fan.ambr | 64 +++++++ tests/components/switchbot_cloud/test_fan.py | 172 +++++++++++++----- 9 files changed, 361 insertions(+), 54 deletions(-) create mode 100644 homeassistant/components/switchbot_cloud/icons.json create mode 100644 tests/components/switchbot_cloud/fixtures/air_purifier_status.json create mode 100644 tests/components/switchbot_cloud/snapshots/test_fan.ambr diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index 44fbfe0fcf4ff..edf30984fe633 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -184,6 +184,11 @@ async def make_device_data( devices_data.buttons.append((device, coordinator)) else: devices_data.switches.append((device, coordinator)) + if isinstance(device, Device) and device.device_type.startswith("Air Purifier"): + coordinator = await coordinator_for_device( + hass, entry, api, device, coordinators_by_id + ) + devices_data.fans.append((device, coordinator)) if isinstance(device, Device) and device.device_type in [ "Battery Circulator Fan", diff --git a/homeassistant/components/switchbot_cloud/const.py b/homeassistant/components/switchbot_cloud/const.py index a9b3d0df412e5..23a212075c4ec 100644 --- a/homeassistant/components/switchbot_cloud/const.py +++ b/homeassistant/components/switchbot_cloud/const.py @@ -1,6 +1,7 @@ """Constants for the SwitchBot Cloud integration.""" from datetime import timedelta +from enum import Enum from typing import Final DOMAIN: Final = "switchbot_cloud" @@ -17,5 +18,18 @@ VACUUM_FAN_SPEED_MAX = "max" AFTER_COMMAND_REFRESH = 5 - COVER_ENTITY_AFTER_COMMAND_REFRESH = 10 + + +class AirPurifierMode(Enum): + """Air Purifier Modes.""" + + NORMAL = 1 + AUTO = 2 + SLEEP = 3 + PET = 4 + + @classmethod + def get_modes(cls) -> list[str]: + """Return a list of available air purifier modes as lowercase strings.""" + return [mode.name.lower() for mode in cls] diff --git a/homeassistant/components/switchbot_cloud/fan.py b/homeassistant/components/switchbot_cloud/fan.py index 418296ffb556b..9424b5478ace1 100644 --- a/homeassistant/components/switchbot_cloud/fan.py +++ b/homeassistant/components/switchbot_cloud/fan.py @@ -1,23 +1,30 @@ """Support for the Switchbot Battery Circulator fan.""" import asyncio +import logging from typing import Any from switchbot_api import ( + AirPurifierCommands, BatteryCirculatorFanCommands, BatteryCirculatorFanMode, CommonCommands, + SwitchBotAPI, ) from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import SwitchbotCloudData -from .const import AFTER_COMMAND_REFRESH, DOMAIN +from .const import AFTER_COMMAND_REFRESH, DOMAIN, AirPurifierMode from .entity import SwitchBotCloudEntity +_LOGGER = logging.getLogger(__name__) +PARALLEL_UPDATES = 0 + async def async_setup_entry( hass: HomeAssistant, @@ -26,10 +33,13 @@ async def async_setup_entry( ) -> None: """Set up SwitchBot Cloud entry.""" data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id] - async_add_entities( - SwitchBotCloudFan(data.api, device, coordinator) - for device, coordinator in data.devices.fans - ) + for device, coordinator in data.devices.fans: + if device.device_type.startswith("Air Purifier"): + async_add_entities( + [SwitchBotAirPurifierEntity(data.api, device, coordinator)] + ) + else: + async_add_entities([SwitchBotCloudFan(data.api, device, coordinator)]) class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity): @@ -37,6 +47,7 @@ class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity): _attr_name = None + _api: SwitchBotAPI _attr_supported_features = ( FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE @@ -118,3 +129,75 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: ) await asyncio.sleep(AFTER_COMMAND_REFRESH) await self.coordinator.async_request_refresh() + + +class SwitchBotAirPurifierEntity(SwitchBotCloudEntity, FanEntity): + """Representation of a Switchbot air purifier.""" + + _api: SwitchBotAPI + _attr_supported_features = ( + FanEntityFeature.PRESET_MODE + | FanEntityFeature.TURN_OFF + | FanEntityFeature.TURN_ON + ) + _attr_preset_modes = AirPurifierMode.get_modes() + _attr_translation_key = "air_purifier" + _attr_name = None + _attr_is_on: bool | None = None + + @property + def is_on(self) -> bool | None: + """Return true if device is on.""" + return self._attr_is_on + + def _set_attributes(self) -> None: + """Set attributes from coordinator data.""" + if self.coordinator.data is None: + return + + self._attr_is_on = self.coordinator.data.get("power") == STATE_ON.upper() + mode = self.coordinator.data.get("mode") + self._attr_preset_mode = ( + AirPurifierMode(mode).name.lower() if mode is not None else None + ) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode of the air purifier.""" + + _LOGGER.debug( + "Switchbot air purifier to set preset mode %s %s", + preset_mode, + self._attr_unique_id, + ) + await self.send_api_command( + AirPurifierCommands.SET_MODE, + parameters={"mode": AirPurifierMode[preset_mode.upper()].value}, + ) + await asyncio.sleep(AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs: Any, + ) -> None: + """Turn on the air purifier.""" + + _LOGGER.debug( + "Switchbot air purifier to set turn on %s %s %s", + percentage, + preset_mode, + self._attr_unique_id, + ) + await self.send_api_command(CommonCommands.ON) + await asyncio.sleep(AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the air purifier.""" + + _LOGGER.debug("Switchbot air purifier to set turn off %s", self._attr_unique_id) + await self.send_api_command(CommonCommands.OFF) + await asyncio.sleep(AFTER_COMMAND_REFRESH) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/switchbot_cloud/icons.json b/homeassistant/components/switchbot_cloud/icons.json new file mode 100644 index 0000000000000..2a13cbe75797d --- /dev/null +++ b/homeassistant/components/switchbot_cloud/icons.json @@ -0,0 +1,22 @@ +{ + "entity": { + "fan": { + "air_purifier": { + "default": "mdi:air-purifier", + "state": { + "off": "mdi:air-purifier-off" + }, + "state_attributes": { + "preset_mode": { + "state": { + "normal": "mdi:fan", + "auto": "mdi:auto-mode", + "pet": "mdi:paw", + "sleep": "mdi:power-sleep" + } + } + } + } + } + } +} diff --git a/homeassistant/components/switchbot_cloud/strings.json b/homeassistant/components/switchbot_cloud/strings.json index 11e92e6dfa38f..adb7de00682f2 100644 --- a/homeassistant/components/switchbot_cloud/strings.json +++ b/homeassistant/components/switchbot_cloud/strings.json @@ -16,5 +16,21 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } + }, + "entity": { + "fan": { + "air_purifier": { + "state_attributes": { + "preset_mode": { + "state": { + "normal": "[%key:common::state::normal%]", + "auto": "[%key:common::state::auto%]", + "pet": "Pet", + "sleep": "Sleep" + } + } + } + } + } } } diff --git a/tests/components/switchbot_cloud/__init__.py b/tests/components/switchbot_cloud/__init__.py index 42fe3e4f5438d..b0d1c29f4a95d 100644 --- a/tests/components/switchbot_cloud/__init__.py +++ b/tests/components/switchbot_cloud/__init__.py @@ -1,5 +1,7 @@ """Tests for the SwitchBot Cloud integration.""" +from switchbot_api import Device + from homeassistant.components.switchbot_cloud.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN from homeassistant.core import HomeAssistant @@ -21,3 +23,20 @@ async def configure_integration(hass: HomeAssistant) -> MockConfigEntry: await hass.async_block_till_done() return entry + + +AIR_PURIFIER_INFO = Device( + version="V1.0", + deviceId="air-purifier-id-1", + deviceName="air-purifier-1", + deviceType="Air Purifier Table PM2.5", + hubDeviceId="test-hub-id", +) + +CIRCULATOR_FAN_INFO = Device( + version="V1.0", + deviceId="battery-fan-id-1", + deviceName="battery-fan-1", + deviceType="Battery Circulator Fan", + hubDeviceId="test-hub-id", +) diff --git a/tests/components/switchbot_cloud/fixtures/air_purifier_status.json b/tests/components/switchbot_cloud/fixtures/air_purifier_status.json new file mode 100644 index 0000000000000..b490c1c966c14 --- /dev/null +++ b/tests/components/switchbot_cloud/fixtures/air_purifier_status.json @@ -0,0 +1,8 @@ +{ + "version": "V2.3", + "power": "ON", + "mode": 2, + "deviceId": "air-purifier-id-1", + "deviceType": "Air Purifier Table PM2.5", + "hubDeviceId": "test-hub-id" +} diff --git a/tests/components/switchbot_cloud/snapshots/test_fan.ambr b/tests/components/switchbot_cloud/snapshots/test_fan.ambr new file mode 100644 index 0000000000000..e5139527aca18 --- /dev/null +++ b/tests/components/switchbot_cloud/snapshots/test_fan.ambr @@ -0,0 +1,64 @@ +# serializer version: 1 +# name: test_air_purifier[fan.air_purifier_1-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'preset_modes': list([ + 'normal', + 'auto', + 'sleep', + 'pet', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'fan', + 'entity_category': None, + 'entity_id': 'fan.air_purifier_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': None, + 'platform': 'switchbot_cloud', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'air_purifier', + 'unique_id': 'air-purifier-id-1', + 'unit_of_measurement': None, + }) +# --- +# name: test_air_purifier[fan.air_purifier_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'air-purifier-1', + 'preset_mode': 'auto', + 'preset_modes': list([ + 'normal', + 'auto', + 'sleep', + 'pet', + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'fan.air_purifier_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/switchbot_cloud/test_fan.py b/tests/components/switchbot_cloud/test_fan.py index 4a9eb52781818..9852096511a3c 100644 --- a/tests/components/switchbot_cloud/test_fan.py +++ b/tests/components/switchbot_cloud/test_fan.py @@ -2,7 +2,10 @@ from unittest.mock import patch +import pytest +import switchbot_api from switchbot_api import Device, SwitchBotAPI +from syrupy.assertion import SnapshotAssertion from homeassistant.components.fan import ( ATTR_PERCENTAGE, @@ -12,6 +15,7 @@ SERVICE_SET_PRESET_MODE, SERVICE_TURN_ON, ) +from homeassistant.components.switchbot_cloud.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, @@ -19,32 +23,37 @@ STATE_OFF, STATE_ON, STATE_UNKNOWN, + Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er -from . import configure_integration +from . import AIR_PURIFIER_INFO, CIRCULATOR_FAN_INFO, configure_integration +from tests.common import async_load_json_object_fixture, snapshot_platform + +@pytest.mark.parametrize( + ("device_info", "entry_id"), + [ + (AIR_PURIFIER_INFO, "fan.air_purifier_1"), + (CIRCULATOR_FAN_INFO, "fan.battery_fan_1"), + ], +) async def test_coordinator_data_is_none( - hass: HomeAssistant, mock_list_devices, mock_get_status + hass: HomeAssistant, + mock_list_devices, + mock_get_status, + device_info: Device, + entry_id: str, ) -> None: """Test coordinator data is none.""" - mock_list_devices.return_value = [ - Device( - version="V1.0", - deviceId="battery-fan-id-1", - deviceName="battery-fan-1", - deviceType="Battery Circulator Fan", - hubDeviceId="test-hub-id", - ), - ] - mock_get_status.side_effect = [ - None, - ] + mock_list_devices.return_value = [device_info] + mock_get_status.side_effect = [None] + entry = await configure_integration(hass) assert entry.state is ConfigEntryState.LOADED - entity_id = "fan.battery_fan_1" - state = hass.states.get(entity_id) + state = hass.states.get(entry_id) assert state.state == STATE_UNKNOWN @@ -52,13 +61,7 @@ async def test_coordinator_data_is_none( async def test_turn_on(hass: HomeAssistant, mock_list_devices, mock_get_status) -> None: """Test turning on the fan.""" mock_list_devices.return_value = [ - Device( - version="V1.0", - deviceId="battery-fan-id-1", - deviceName="battery-fan-1", - deviceType="Battery Circulator Fan", - hubDeviceId="test-hub-id", - ), + CIRCULATOR_FAN_INFO, ] mock_get_status.side_effect = [ {"power": "off", "mode": "direct", "fanSpeed": "0"}, @@ -72,7 +75,9 @@ async def test_turn_on(hass: HomeAssistant, mock_list_devices, mock_get_status) assert state.state == STATE_OFF - with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + ): await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True ) @@ -87,13 +92,7 @@ async def test_turn_off( ) -> None: """Test turning off the fan.""" mock_list_devices.return_value = [ - Device( - version="V1.0", - deviceId="battery-fan-id-1", - deviceName="battery-fan-1", - deviceType="Battery Circulator Fan", - hubDeviceId="test-hub-id", - ), + CIRCULATOR_FAN_INFO, ] mock_get_status.side_effect = [ {"power": "on", "mode": "direct", "fanSpeed": "0"}, @@ -107,7 +106,9 @@ async def test_turn_off( assert state.state == STATE_ON - with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + ): await hass.services.async_call( FAN_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True ) @@ -122,13 +123,7 @@ async def test_set_percentage( ) -> None: """Test set percentage.""" mock_list_devices.return_value = [ - Device( - version="V1.0", - deviceId="battery-fan-id-1", - deviceName="battery-fan-1", - deviceType="Battery Circulator Fan", - hubDeviceId="test-hub-id", - ), + CIRCULATOR_FAN_INFO, ] mock_get_status.side_effect = [ {"power": "on", "mode": "direct", "fanSpeed": "0"}, @@ -142,7 +137,9 @@ async def test_set_percentage( assert state.state == STATE_ON - with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + ): await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PERCENTAGE, @@ -157,13 +154,7 @@ async def test_set_preset_mode( ) -> None: """Test set preset mode.""" mock_list_devices.return_value = [ - Device( - version="V1.0", - deviceId="battery-fan-id-1", - deviceName="battery-fan-1", - deviceType="Battery Circulator Fan", - hubDeviceId="test-hub-id", - ), + CIRCULATOR_FAN_INFO, ] mock_get_status.side_effect = [ {"power": "on", "mode": "direct", "fanSpeed": "0"}, @@ -177,7 +168,9 @@ async def test_set_preset_mode( assert state.state == STATE_ON - with patch.object(SwitchBotAPI, "send_command") as mock_send_command: + with ( + patch.object(SwitchBotAPI, "send_command") as mock_send_command, + ): await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -185,3 +178,86 @@ async def test_set_preset_mode( blocking=True, ) mock_send_command.assert_called_once() + + +async def test_air_purifier( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_list_devices, + mock_get_status, +) -> None: + """Test air purifier.""" + + mock_list_devices.return_value = [AIR_PURIFIER_INFO] + mock_get_status.return_value = await async_load_json_object_fixture( + hass, "air_purifier_status.json", DOMAIN + ) + + with patch("homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.FAN]): + entry = await configure_integration(hass) + + await snapshot_platform(hass, entity_registry, snapshot, entry.entry_id) + + +@pytest.mark.parametrize( + ("service", "service_data", "expected_call_args"), + [ + ( + "turn_on", + {}, + ( + "air-purifier-id-1", + switchbot_api.CommonCommands.ON, + "command", + "default", + ), + ), + ( + "turn_off", + {}, + ( + "air-purifier-id-1", + switchbot_api.CommonCommands.OFF, + "command", + "default", + ), + ), + ( + "set_preset_mode", + {"preset_mode": "sleep"}, + ( + "air-purifier-id-1", + switchbot_api.AirPurifierCommands.SET_MODE, + "command", + {"mode": 3}, + ), + ), + ], +) +async def test_air_purifier_controller( + hass: HomeAssistant, + mock_list_devices, + mock_get_status, + service: str, + service_data: dict, + expected_call_args: tuple, +) -> None: + """Test controlling the air purifier with mocked delay.""" + mock_list_devices.return_value = [AIR_PURIFIER_INFO] + mock_get_status.return_value = {"power": "OFF", "mode": 2} + + await configure_integration(hass) + fan_id = "fan.air_purifier_1" + + with ( + patch.object(SwitchBotAPI, "send_command") as mocked_send_command, + ): + await hass.services.async_call( + FAN_DOMAIN, + service, + {**service_data, ATTR_ENTITY_ID: fan_id}, + blocking=True, + ) + + mocked_send_command.assert_awaited_once_with(*expected_call_args) From 76f3397aa036073b9262f4fd10ebc507aa08e80e Mon Sep 17 00:00:00 2001 From: Paul Warren Date: Tue, 19 Aug 2025 14:12:53 +0000 Subject: [PATCH 14/23] Bump pyDaikin to 2.16.0 (#150867) --- homeassistant/components/daikin/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index 947fe514747e6..799ff378a358e 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/daikin", "iot_class": "local_polling", "loggers": ["pydaikin"], - "requirements": ["pydaikin==2.15.0"], + "requirements": ["pydaikin==2.16.0"], "zeroconf": ["_dkapi._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index af2db386a6493..de446ac232251 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1906,7 +1906,7 @@ pycsspeechtts==1.0.8 # pycups==2.0.4 # homeassistant.components.daikin -pydaikin==2.15.0 +pydaikin==2.16.0 # homeassistant.components.danfoss_air pydanfossair==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8735fbd403987..3fd4773dffd35 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1596,7 +1596,7 @@ pycsspeechtts==1.0.8 # pycups==2.0.4 # homeassistant.components.daikin -pydaikin==2.15.0 +pydaikin==2.16.0 # homeassistant.components.deako pydeako==0.6.0 From b52a806b3615826538343000364436309ba8d25e Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:23:30 +0200 Subject: [PATCH 15/23] Show charging power as 0 when not charging for the Volvo integration (#150797) --- homeassistant/components/volvo/coordinator.py | 29 ++++- homeassistant/components/volvo/sensor.py | 4 +- tests/components/volvo/conftest.py | 4 +- .../ex30_2024/energy_capabilities.json | 4 +- .../fixtures/ex30_2024/energy_state.json | 4 +- .../energy_capabilities.json | 2 +- .../xc60_phev_2020/energy_capabilities.json | 2 +- .../fixtures/xc60_phev_2020/energy_state.json | 7 +- .../volvo/snapshots/test_sensor.ambr | 110 +++++++++++++++++- tests/components/volvo/test_sensor.py | 18 ++- 10 files changed, 168 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/volvo/coordinator.py b/homeassistant/components/volvo/coordinator.py index da23e7875c917..d6c8f349a52a2 100644 --- a/homeassistant/components/volvo/coordinator.py +++ b/homeassistant/components/volvo/coordinator.py @@ -260,6 +260,8 @@ def __init__( "Volvo medium interval coordinator", ) + self._supported_capabilities: list[str] = [] + async def _async_determine_api_calls( self, ) -> list[Callable[[], Coroutine[Any, Any, Any]]]: @@ -267,6 +269,31 @@ async def _async_determine_api_calls( capabilities = await self.api.async_get_energy_capabilities() if capabilities.get("isSupported", False): - return [self.api.async_get_energy_state] + self._supported_capabilities = [ + key + for key, value in capabilities.items() + if isinstance(value, dict) and value.get("isSupported", False) + ] + + return [self._async_get_energy_state] return [] + + async def _async_get_energy_state( + self, + ) -> dict[str, VolvoCarsValueStatusField | None]: + def _mark_ok( + field: VolvoCarsValueStatusField | None, + ) -> VolvoCarsValueStatusField | None: + if field: + field.status = "OK" + + return field + + energy_state = await self.api.async_get_energy_state() + + return { + key: _mark_ok(value) + for key, value in energy_state.items() + if key in self._supported_capabilities + } diff --git a/homeassistant/components/volvo/sensor.py b/homeassistant/components/volvo/sensor.py index 7f37ac42dc8c3..b9a620d898d8a 100644 --- a/homeassistant/components/volvo/sensor.py +++ b/homeassistant/components/volvo/sensor.py @@ -67,8 +67,8 @@ def _calculate_time_to_service(field: VolvoCarsValue) -> int: def _charging_power_value(field: VolvoCarsValue) -> int: return ( - int(field.value) - if isinstance(field, VolvoCarsValueStatusField) and field.status == "OK" + field.value + if isinstance(field, VolvoCarsValueStatusField) and isinstance(field.value, int) else 0 ) diff --git a/tests/components/volvo/conftest.py b/tests/components/volvo/conftest.py index edd3f39998e9f..fedd3a6ec3f16 100644 --- a/tests/components/volvo/conftest.py +++ b/tests/components/volvo/conftest.py @@ -9,7 +9,7 @@ from volvocarsapi.models import ( VolvoCarsAvailableCommand, VolvoCarsLocation, - VolvoCarsValueField, + VolvoCarsValueStatusField, VolvoCarsVehicle, ) @@ -98,7 +98,7 @@ async def mock_api(hass: HomeAssistant, full_model: str) -> AsyncGenerator[Async hass, "energy_state", full_model ) energy_state = { - key: VolvoCarsValueField.from_dict(value) + key: VolvoCarsValueStatusField.from_dict(value) for key, value in energy_state_data.items() } engine_status = await async_load_fixture_as_value_field( diff --git a/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json b/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json index 968c759ab2748..f3aff11585d6c 100644 --- a/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json +++ b/tests/components/volvo/fixtures/ex30_2024/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { @@ -25,7 +25,7 @@ "isSupported": true }, "chargingCurrentLimit": { - "isSupported": true + "isSupported": false }, "chargingPower": { "isSupported": true diff --git a/tests/components/volvo/fixtures/ex30_2024/energy_state.json b/tests/components/volvo/fixtures/ex30_2024/energy_state.json index 0170d1aa617f3..5973100d4ea8c 100644 --- a/tests/components/volvo/fixtures/ex30_2024/energy_state.json +++ b/tests/components/volvo/fixtures/ex30_2024/energy_state.json @@ -50,7 +50,7 @@ }, "chargingPower": { "status": "ERROR", - "code": "NOT_SUPPORTED", - "message": "Resource is not supported for this vehicle" + "code": "PROPERTY_NOT_FOUND", + "message": "No valid value could be found for the requested property" } } diff --git a/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json b/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json index 968c759ab2748..3523d51e0717c 100644 --- a/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json +++ b/tests/components/volvo/fixtures/xc40_electric_2024/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json b/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json index d8aa07ff0bb65..331795f545b70 100644 --- a/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json +++ b/tests/components/volvo/fixtures/xc60_phev_2020/energy_capabilities.json @@ -9,7 +9,7 @@ "chargerConnectionStatus": { "isSupported": true }, - "chargingSystemStatus": { + "chargingStatus": { "isSupported": true }, "chargingType": { diff --git a/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json b/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json index e2f0cd13807b0..e198bfc833095 100644 --- a/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json +++ b/tests/components/volvo/fixtures/xc60_phev_2020/energy_state.json @@ -40,9 +40,10 @@ "message": "Resource is not supported for this vehicle" }, "targetBatteryChargeLevel": { - "status": "ERROR", - "code": "NOT_SUPPORTED", - "message": "Resource is not supported for this vehicle" + "status": "OK", + "value": 80, + "unit": "percentage", + "updatedAt": "2024-09-22T09:40:12Z" }, "chargingPower": { "status": "ERROR", diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index e218986517ab3..9d709a27fc3d8 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -232,6 +232,62 @@ 'state': 'connected', }) # --- +# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_ex30_charging_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging power', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'charging_power', + 'unique_id': 'yv1abcdefg1234567_charging_power', + 'unit_of_measurement': , + }) +# --- +# name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Volvo EX30 Charging power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.volvo_ex30_charging_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_charging_power_status-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2164,7 +2220,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': '1386', }) # --- # name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_charging_power_status-entry] @@ -3601,6 +3657,58 @@ 'state': '30000', }) # --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_target_battery_charge_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Target battery charge level', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'target_battery_charge_level', + 'unique_id': 'yv1abcdefg1234567_target_battery_charge_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo XC60 Target battery charge level', + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_target_battery_charge_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '80', + }) +# --- # name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_time_to_engine_service-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/volvo/test_sensor.py b/tests/components/volvo/test_sensor.py index 2813c74128625..a4b7a787117ce 100644 --- a/tests/components/volvo/test_sensor.py +++ b/tests/components/volvo/test_sensor.py @@ -68,4 +68,20 @@ async def test_skip_invalid_api_fields( with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): assert await setup_integration() - assert not hass.states.get(f"sensor.volvo_{short_model}_charging_power") + assert not hass.states.get(f"sensor.volvo_{short_model}_charging_current_limit") + + +@pytest.mark.parametrize( + "full_model", + ["ex30_2024"], +) +async def test_charging_power_value( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], +) -> None: + """Test if charging_power_value is zero if supported, but not charging.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]): + assert await setup_integration() + + assert hass.states.get("sensor.volvo_ex30_charging_power").state == "0" From f4400516b8e611a1884354273d53802e6b7f71c2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 19 Aug 2025 16:24:32 +0200 Subject: [PATCH 16/23] Fix PWA theme color to match darker blue color scheme in 2025.8 (#150896) Co-authored-by: Claude --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 2f2a8e93b1e0d..ff50567257ac5 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -50,7 +50,7 @@ CONF_FRONTEND_REPO = "development_repo" CONF_JS_VERSION = "javascript_version" -DEFAULT_THEME_COLOR = "#03A9F4" +DEFAULT_THEME_COLOR = "#2980b9" DATA_PANELS: HassKey[dict[str, Panel]] = HassKey("frontend_panels") From 63640af4d4a1844b1d2c07fff7a4f67c470b089e Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Tue, 19 Aug 2025 16:42:49 +0200 Subject: [PATCH 17/23] Update voluptuous-serialize to 2.7.0 (#150822) --- homeassistant/components/auth/login_flow.py | 22 ++++---- .../components/auth/mfa_setup_flow.py | 18 +++---- .../components/config/config_entries.py | 22 ++++---- .../components/repairs/websocket_api.py | 6 +-- homeassistant/helpers/data_entry_flow.py | 24 ++++----- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- .../alarm_control_panel/test_device_action.py | 48 ++++++++++++----- .../test_device_trigger.py | 14 ++++- .../binary_sensor/test_device_condition.py | 14 ++++- .../binary_sensor/test_device_trigger.py | 14 ++++- .../components/climate/test_device_trigger.py | 16 +++++- .../components/config/test_config_entries.py | 14 +++-- tests/components/cover/test_device_action.py | 2 + .../components/cover/test_device_condition.py | 4 ++ tests/components/cover/test_device_trigger.py | 20 +++++++- .../components/device_automation/test_init.py | 18 +++++-- tests/components/fan/test_device_trigger.py | 14 ++++- tests/components/hassio/test_repairs.py | 5 ++ .../humidifier/test_device_condition.py | 8 +++ .../humidifier/test_device_trigger.py | 27 ++++++++-- tests/components/knx/test_device_trigger.py | 6 +++ tests/components/lcn/test_device_trigger.py | 47 +++++++++++++++-- tests/components/light/test_device_action.py | 8 +++ .../components/light/test_device_condition.py | 14 ++++- tests/components/light/test_device_trigger.py | 14 ++++- tests/components/lock/test_device_trigger.py | 14 ++++- .../media_player/test_device_trigger.py | 14 ++++- .../remote/test_device_condition.py | 14 ++++- .../components/remote/test_device_trigger.py | 14 ++++- tests/components/select/test_device_action.py | 4 ++ .../select/test_device_condition.py | 4 ++ .../components/select/test_device_trigger.py | 15 ++++++ .../sensor/test_device_condition.py | 4 ++ .../components/sensor/test_device_trigger.py | 18 ++++++- .../switch/test_device_condition.py | 14 ++++- .../components/switch/test_device_trigger.py | 14 ++++- .../components/update/test_device_trigger.py | 14 ++++- .../components/vacuum/test_device_trigger.py | 14 ++++- tests/components/zha/data.py | 6 +++ tests/components/zha/test_helpers.py | 2 + .../components/zwave_js/test_device_action.py | 22 ++++++-- .../zwave_js/test_device_condition.py | 4 +- .../zwave_js/test_device_trigger.py | 51 +++++++++++++++---- tests/test_data_entry_flow.py | 8 ++- 46 files changed, 519 insertions(+), 136 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index fe7ccededf2f9..69ae3eb65bd4b 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -199,23 +199,19 @@ async def get(self, request: web.Request) -> web.Response: ) -def _prepare_result_json( - result: AuthFlowResult, -) -> AuthFlowResult: - """Convert result to JSON.""" +def _prepare_result_json(result: AuthFlowResult) -> dict[str, Any]: + """Convert result to JSON serializable dict.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: - data = result.copy() - data.pop("result") - data.pop("data") - return data + return { + key: val for key, val in result.items() if key not in ("result", "data") + } if result["type"] != data_entry_flow.FlowResultType.FORM: - return result + return result # type: ignore[return-value] - data = result.copy() - - if (schema := data["data_schema"]) is None: - data["data_schema"] = [] # type: ignore[typeddict-item] # json result type + data = dict(result) + if (schema := result["data_schema"]) is None: + data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert(schema) diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 6c85f5b7f556d..5b4a539b86f8b 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -149,20 +149,16 @@ async def async_depose(msg: dict[str, Any]) -> None: hass.async_create_task(async_depose(msg)) -def _prepare_result_json( - result: data_entry_flow.FlowResult, -) -> data_entry_flow.FlowResult: - """Convert result to JSON.""" +def _prepare_result_json(result: data_entry_flow.FlowResult) -> dict[str, Any]: + """Convert result to JSON serializable dict.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: - return result.copy() - + return dict(result) if result["type"] != data_entry_flow.FlowResultType.FORM: - return result - - data = result.copy() + return result # type: ignore[return-value] - if (schema := data["data_schema"]) is None: - data["data_schema"] = [] # type: ignore[typeddict-item] # json result type + data = dict(result) + if (schema := result["data_schema"]) is None: + data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert(schema) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index a9aafcfaa5e71..176c9e2b04703 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -137,20 +137,16 @@ async def post(self, request: web.Request, entry_id: str) -> web.Response: def _prepare_config_flow_result_json( result: data_entry_flow.FlowResult, - prepare_result_json: Callable[ - [data_entry_flow.FlowResult], data_entry_flow.FlowResult - ], -) -> data_entry_flow.FlowResult: + prepare_result_json: Callable[[data_entry_flow.FlowResult], dict[str, Any]], +) -> dict[str, Any]: """Convert result to JSON.""" if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY: return prepare_result_json(result) - data = result.copy() - entry: config_entries.ConfigEntry = data["result"] # type: ignore[typeddict-item] + data = {key: val for key, val in result.items() if key not in ("data", "context")} + entry: config_entries.ConfigEntry = result["result"] # type: ignore[typeddict-item] # We overwrite the ConfigEntry object with its json representation. - data["result"] = entry.as_json_fragment # type: ignore[typeddict-unknown-key] - data.pop("data") - data.pop("context") + data["result"] = entry.as_json_fragment return data @@ -204,8 +200,8 @@ def get_context(self, data: dict[str, Any]) -> dict[str, Any]: def _prepare_result_json( self, result: data_entry_flow.FlowResult - ) -> data_entry_flow.FlowResult: - """Convert result to JSON.""" + ) -> dict[str, Any]: + """Convert result to JSON serializable dict.""" return _prepare_config_flow_result_json(result, super()._prepare_result_json) @@ -229,8 +225,8 @@ async def post(self, request: web.Request, flow_id: str) -> web.Response: def _prepare_result_json( self, result: data_entry_flow.FlowResult - ) -> data_entry_flow.FlowResult: - """Convert result to JSON.""" + ) -> dict[str, Any]: + """Convert result to JSON serializable dict.""" return _prepare_config_flow_result_json(result, super()._prepare_result_json) diff --git a/homeassistant/components/repairs/websocket_api.py b/homeassistant/components/repairs/websocket_api.py index 4117b0ee35b90..d09c567bb71e9 100644 --- a/homeassistant/components/repairs/websocket_api.py +++ b/homeassistant/components/repairs/websocket_api.py @@ -137,9 +137,9 @@ async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response "Handler does not support user", HTTPStatus.BAD_REQUEST ) - result = self._prepare_result_json(result) - - return self.json(result) + return self.json( + self._prepare_result_json(result), + ) class RepairsFlowResourceView(FlowManagerResourceView): diff --git a/homeassistant/helpers/data_entry_flow.py b/homeassistant/helpers/data_entry_flow.py index 22074fb90a72c..9ace020f342a9 100644 --- a/homeassistant/helpers/data_entry_flow.py +++ b/homeassistant/helpers/data_entry_flow.py @@ -31,27 +31,27 @@ def __init__(self, flow_mgr: _FlowManagerT) -> None: def _prepare_result_json( self, result: data_entry_flow.FlowResult - ) -> data_entry_flow.FlowResult: - """Convert result to JSON.""" + ) -> dict[str, Any]: + """Convert result to JSON serializable dict.""" if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY: - data = result.copy() assert "result" not in result - data.pop("data") - data.pop("context") - return data + return { + key: val + for key, val in result.items() + if key not in ("data", "context") + } - if "data_schema" not in result: - return result + data = dict(result) - data = result.copy() + if "data_schema" not in result: + return data - if (schema := data["data_schema"]) is None: - data["data_schema"] = [] # type: ignore[typeddict-item] # json result type + if (schema := result["data_schema"]) is None: + data["data_schema"] = [] else: data["data_schema"] = voluptuous_serialize.convert( schema, custom_serializer=cv.custom_serializer ) - return data diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e3c1bf4efe869..4dfb2dad73474 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -70,7 +70,7 @@ ulid-transform==1.4.0 urllib3>=2.0 uv==0.8.9 voluptuous-openapi==0.1.0 -voluptuous-serialize==2.6.0 +voluptuous-serialize==2.7.0 voluptuous==0.15.2 webrtc-models==0.3.0 yarl==1.20.1 diff --git a/pyproject.toml b/pyproject.toml index 3e24035f27137..30860e381d354 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ dependencies = [ "urllib3>=2.0", "uv==0.8.9", "voluptuous==0.15.2", - "voluptuous-serialize==2.6.0", + "voluptuous-serialize==2.7.0", "voluptuous-openapi==0.1.0", "yarl==1.20.1", "webrtc-models==0.3.0", diff --git a/requirements.txt b/requirements.txt index f053bc0d541c0..143df76e63692 100644 --- a/requirements.txt +++ b/requirements.txt @@ -48,7 +48,7 @@ ulid-transform==1.4.0 urllib3>=2.0 uv==0.8.9 voluptuous==0.15.2 -voluptuous-serialize==2.6.0 +voluptuous-serialize==2.7.0 voluptuous-openapi==0.1.0 yarl==1.20.1 webrtc-models==0.3.0 diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index a733501769181..d52ee5733a1b8 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -245,7 +245,9 @@ async def test_get_action_capabilities( "arm_night": {"extra_fields": []}, "arm_vacation": {"extra_fields": []}, "disarm": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "trigger": {"extra_fields": []}, } @@ -293,7 +295,9 @@ async def test_get_action_capabilities_legacy( "arm_night": {"extra_fields": []}, "arm_vacation": {"extra_fields": []}, "disarm": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "trigger": {"extra_fields": []}, } @@ -338,19 +342,29 @@ async def test_get_action_capabilities_arm_code( expected_capabilities = { "arm_away": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_home": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_night": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_vacation": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "disarm": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "trigger": {"extra_fields": []}, } @@ -394,19 +408,29 @@ async def test_get_action_capabilities_arm_code_legacy( expected_capabilities = { "arm_away": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_home": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_night": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "arm_vacation": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "disarm": { - "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + "extra_fields": [ + {"name": "code", "optional": True, "required": False, "type": "string"} + ] }, "trigger": {"extra_fields": []}, } diff --git a/tests/components/alarm_control_panel/test_device_trigger.py b/tests/components/alarm_control_panel/test_device_trigger.py index 3efacb8056069..979bc33bb00ea 100644 --- a/tests/components/alarm_control_panel/test_device_trigger.py +++ b/tests/components/alarm_control_panel/test_device_trigger.py @@ -191,7 +191,12 @@ async def test_get_trigger_capabilities( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -226,7 +231,12 @@ async def test_get_trigger_capabilities_legacy( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 59fbdf9a25324..254c5428806ae 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -182,7 +182,12 @@ async def test_get_condition_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( @@ -212,7 +217,12 @@ async def test_get_condition_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index dd71c1e5d065c..e9ad5d0a1e17a 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -185,7 +185,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -215,7 +220,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/climate/test_device_trigger.py b/tests/components/climate/test_device_trigger.py index 4b5a578ecc456..06072b88afec9 100644 --- a/tests/components/climate/test_device_trigger.py +++ b/tests/components/climate/test_device_trigger.py @@ -354,7 +354,12 @@ async def test_get_trigger_capabilities_hvac_mode(hass: HomeAssistant) -> None: "required": True, "type": "select", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] @@ -389,13 +394,20 @@ async def test_get_trigger_capabilities_temp_humid( "description": {"suffix": suffix}, "name": "above", "optional": True, + "required": False, "type": "float", }, { "description": {"suffix": suffix}, "name": "below", "optional": True, + "required": False, "type": "float", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index c6e82976bf111..8f89549944c94 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -625,7 +625,9 @@ async def async_step_account(self, user_input=None): "type": "form", "handler": "test", "step_id": "account", - "data_schema": [{"name": "user_title", "type": "string"}], + "data_schema": [ + {"name": "user_title", "required": False, "type": "string"} + ], "description_placeholders": None, "errors": None, "last_step": None, @@ -712,7 +714,9 @@ async def async_step_account(self, user_input=None): "type": "form", "handler": "test", "step_id": "account", - "data_schema": [{"name": "user_title", "type": "string"}], + "data_schema": [ + {"name": "user_title", "required": False, "type": "string"} + ], "description_placeholders": None, "errors": None, "last_step": None, @@ -1272,7 +1276,7 @@ async def async_step_finish(self, user_input=None): "type": "form", "handler": "test1", "step_id": "finish", - "data_schema": [{"name": "enabled", "type": "boolean"}], + "data_schema": [{"name": "enabled", "required": False, "type": "boolean"}], "description_placeholders": None, "errors": None, "last_step": None, @@ -1581,7 +1585,7 @@ def async_get_supported_subentry_types( "type": "form", "handler": ["test1", "test"], "step_id": "finish", - "data_schema": [{"name": "enabled", "type": "boolean"}], + "data_schema": [{"name": "enabled", "required": False, "type": "boolean"}], "description_placeholders": None, "errors": None, "last_step": None, @@ -1749,7 +1753,7 @@ def async_get_supported_subentry_types( data = await resp.json() flow_id = data["flow_id"] expected_data = { - "data_schema": [{"name": "enabled", "type": "boolean"}], + "data_schema": [{"name": "enabled", "required": False, "type": "boolean"}], "description_placeholders": None, "errors": None, "flow_id": flow_id, diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index db9e75bcaef3d..438e5de751d1f 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -260,6 +260,7 @@ async def test_get_action_capabilities_set_pos( { "name": "position", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, @@ -310,6 +311,7 @@ async def test_get_action_capabilities_set_tilt_pos( { "name": "position", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index aa5f150172cf0..5bd0212058565 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -254,6 +254,7 @@ async def test_get_condition_capabilities_set_pos( { "name": "above", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, @@ -262,6 +263,7 @@ async def test_get_condition_capabilities_set_pos( { "name": "below", "optional": True, + "required": False, "type": "integer", "default": 100, "valueMax": 100, @@ -311,6 +313,7 @@ async def test_get_condition_capabilities_set_tilt_pos( { "name": "above", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, @@ -319,6 +322,7 @@ async def test_get_condition_capabilities_set_tilt_pos( { "name": "below", "optional": True, + "required": False, "type": "integer", "default": 100, "valueMax": 100, diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 7901baaa3b8c4..1a6b50b29352f 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -192,7 +192,12 @@ async def test_get_trigger_capabilities( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -230,7 +235,12 @@ async def test_get_trigger_capabilities_legacy( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -262,6 +272,7 @@ async def test_get_trigger_capabilities_set_pos( { "name": "above", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, @@ -270,6 +281,7 @@ async def test_get_trigger_capabilities_set_pos( { "name": "below", "optional": True, + "required": False, "type": "integer", "default": 100, "valueMax": 100, @@ -293,6 +305,7 @@ async def test_get_trigger_capabilities_set_pos( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ] @@ -326,6 +339,7 @@ async def test_get_trigger_capabilities_set_tilt_pos( { "name": "above", "optional": True, + "required": False, "type": "integer", "default": 0, "valueMax": 100, @@ -334,6 +348,7 @@ async def test_get_trigger_capabilities_set_tilt_pos( { "name": "below", "optional": True, + "required": False, "type": "integer", "default": 100, "valueMax": 100, @@ -357,6 +372,7 @@ async def test_get_trigger_capabilities_set_tilt_pos( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ] diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 94625746b05fa..456202a63a43d 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -293,7 +293,9 @@ async def test_websocket_get_action_capabilities( ) expected_capabilities = { "turn_on": { - "extra_fields": [{"type": "string", "name": "code", "optional": True}] + "extra_fields": [ + {"type": "string", "name": "code", "optional": True, "required": False} + ] }, "turn_off": {"extra_fields": []}, "toggle": {"extra_fields": []}, @@ -452,7 +454,12 @@ async def test_websocket_get_condition_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -745,7 +752,12 @@ async def test_websocket_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } diff --git a/tests/components/fan/test_device_trigger.py b/tests/components/fan/test_device_trigger.py index bef44c92f3450..800412fc9d4f0 100644 --- a/tests/components/fan/test_device_trigger.py +++ b/tests/components/fan/test_device_trigger.py @@ -125,7 +125,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -155,7 +160,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 39f9d4580bdad..4234aab40c1f8 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -180,6 +180,7 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions( ["system_execute_reboot", "system_execute_reboot"], ["system_test_type", "system_test_type"], ], + "required": False, "name": "next_step_id", } ], @@ -275,6 +276,7 @@ async def test_supervisor_issue_repair_flow_with_multiple_suggestions_and_confir ["system_execute_reboot", "system_execute_reboot"], ["system_test_type", "system_test_type"], ], + "required": False, "name": "next_step_id", } ], @@ -545,6 +547,7 @@ async def test_mount_failed_repair_flow( ["mount_execute_reload", "mount_execute_reload"], ["mount_execute_remove", "mount_execute_remove"], ], + "required": False, "name": "next_step_id", } ], @@ -756,6 +759,7 @@ async def test_supervisor_issue_repair_flow_multiple_data_disks( ["system_rename_data_disk", "system_rename_data_disk"], ["system_adopt_data_disk", "system_adopt_data_disk"], ], + "required": False, "name": "next_step_id", } ], @@ -960,6 +964,7 @@ async def test_supervisor_issue_addon_boot_fail( ["addon_execute_start", "addon_execute_start"], ["addon_disable_boot", "addon_disable_boot"], ], + "required": False, "name": "next_step_id", } ], diff --git a/tests/components/humidifier/test_device_condition.py b/tests/components/humidifier/test_device_condition.py index ec8406bfe7b82..55a2c6878674b 100644 --- a/tests/components/humidifier/test_device_condition.py +++ b/tests/components/humidifier/test_device_condition.py @@ -363,6 +363,7 @@ async def test_if_state_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -376,6 +377,7 @@ async def test_if_state_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -417,6 +419,7 @@ async def test_if_state_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -430,6 +433,7 @@ async def test_if_state_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -533,6 +537,7 @@ async def test_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -546,6 +551,7 @@ async def test_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -587,6 +593,7 @@ async def test_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], @@ -600,6 +607,7 @@ async def test_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", } ], diff --git a/tests/components/humidifier/test_device_trigger.py b/tests/components/humidifier/test_device_trigger.py index e1b2b2bff611f..6d45861b22753 100644 --- a/tests/components/humidifier/test_device_trigger.py +++ b/tests/components/humidifier/test_device_trigger.py @@ -543,7 +543,14 @@ async def test_get_trigger_capabilities_on(hass: HomeAssistant) -> None: assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "for", "optional": True, "type": "positive_time_period_dict"}] + ) == [ + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } + ] async def test_get_trigger_capabilities_off(hass: HomeAssistant) -> None: @@ -563,7 +570,14 @@ async def test_get_trigger_capabilities_off(hass: HomeAssistant) -> None: assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "for", "optional": True, "type": "positive_time_period_dict"}] + ) == [ + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } + ] async def test_get_trigger_capabilities_humidity(hass: HomeAssistant) -> None: @@ -588,13 +602,20 @@ async def test_get_trigger_capabilities_humidity(hass: HomeAssistant) -> None: "description": {"suffix": "%"}, "name": "above", "optional": True, + "required": False, "type": "integer", }, { "description": {"suffix": "%"}, "name": "below", "optional": True, + "required": False, "type": "integer", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] diff --git a/tests/components/knx/test_device_trigger.py b/tests/components/knx/test_device_trigger.py index e4a208906c662..124ce60e47597 100644 --- a/tests/components/knx/test_device_trigger.py +++ b/tests/components/knx/test_device_trigger.py @@ -301,6 +301,7 @@ async def test_get_trigger_capabilities( { "name": "destination", "optional": True, + "required": False, "selector": { "select": { "custom_value": True, @@ -314,6 +315,7 @@ async def test_get_trigger_capabilities( { "name": "group_value_write", "optional": True, + "required": False, "default": True, "selector": { "boolean": {}, @@ -322,6 +324,7 @@ async def test_get_trigger_capabilities( { "name": "group_value_response", "optional": True, + "required": False, "default": True, "selector": { "boolean": {}, @@ -330,6 +333,7 @@ async def test_get_trigger_capabilities( { "name": "group_value_read", "optional": True, + "required": False, "default": True, "selector": { "boolean": {}, @@ -338,6 +342,7 @@ async def test_get_trigger_capabilities( { "name": "incoming", "optional": True, + "required": False, "default": True, "selector": { "boolean": {}, @@ -346,6 +351,7 @@ async def test_get_trigger_capabilities( { "name": "outgoing", "optional": True, + "required": False, "default": True, "selector": { "boolean": {}, diff --git a/tests/components/lcn/test_device_trigger.py b/tests/components/lcn/test_device_trigger.py index 94eb96591e2bd..9f134a0c20372 100644 --- a/tests/components/lcn/test_device_trigger.py +++ b/tests/components/lcn/test_device_trigger.py @@ -349,7 +349,15 @@ async def test_get_transponder_trigger_capabilities( assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "code", "optional": True, "type": "string", "lower": True}] + ) == [ + { + "name": "code", + "optional": True, + "required": False, + "type": "string", + "lower": True, + } + ] async def test_get_fingerprint_trigger_capabilities( @@ -373,7 +381,15 @@ async def test_get_fingerprint_trigger_capabilities( assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"name": "code", "optional": True, "type": "string", "lower": True}] + ) == [ + { + "name": "code", + "optional": True, + "required": False, + "type": "string", + "lower": True, + } + ] async def test_get_transmitter_trigger_capabilities( @@ -398,13 +414,32 @@ async def test_get_transmitter_trigger_capabilities( assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == [ - {"name": "code", "type": "string", "optional": True, "lower": True}, - {"name": "level", "type": "integer", "optional": True, "valueMin": 0}, - {"name": "key", "type": "integer", "optional": True, "valueMin": 0}, + { + "name": "code", + "type": "string", + "optional": True, + "required": False, + "lower": True, + }, + { + "name": "level", + "type": "integer", + "optional": True, + "required": False, + "valueMin": 0, + }, + { + "name": "key", + "type": "integer", + "optional": True, + "required": False, + "valueMin": 0, + }, { "name": "action", "type": "select", "optional": True, + "required": False, "options": [("hit", "hit"), ("make", "make"), ("break", "break")], }, ] @@ -436,6 +471,7 @@ async def test_get_send_keys_trigger_capabilities( "name": "key", "type": "select", "optional": True, + "required": False, "options": [(send_key.lower(), send_key.lower()) for send_key in SENDKEYS], }, { @@ -445,6 +481,7 @@ async def test_get_send_keys_trigger_capabilities( (key_action.lower(), key_action.lower()) for key_action in KEY_ACTIONS ], "optional": True, + "required": False, }, ] diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index c2ac7087cf065..1f69b586c9b23 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -194,6 +194,7 @@ async def test_get_action_capabilities( { "name": "brightness_pct", "optional": True, + "required": False, "type": "float", "valueMax": 100, "valueMin": 0, @@ -219,6 +220,7 @@ async def test_get_action_capabilities( { "name": "brightness_pct", "optional": True, + "required": False, "type": "float", "valueMax": 100, "valueMin": 0, @@ -238,6 +240,7 @@ async def test_get_action_capabilities( { "name": "flash", "optional": True, + "required": False, "type": "select", "options": [("short", "short"), ("long", "long")], } @@ -256,6 +259,7 @@ async def test_get_action_capabilities( { "name": "flash", "optional": True, + "required": False, "type": "select", "options": [("short", "short"), ("long", "long")], } @@ -341,6 +345,7 @@ async def test_get_action_capabilities_features( { "name": "brightness_pct", "optional": True, + "required": False, "type": "float", "valueMax": 100, "valueMin": 0, @@ -366,6 +371,7 @@ async def test_get_action_capabilities_features( { "name": "brightness_pct", "optional": True, + "required": False, "type": "float", "valueMax": 100, "valueMin": 0, @@ -385,6 +391,7 @@ async def test_get_action_capabilities_features( { "name": "flash", "optional": True, + "required": False, "type": "select", "options": [("short", "short"), ("long", "long")], } @@ -403,6 +410,7 @@ async def test_get_action_capabilities_features( { "name": "flash", "optional": True, + "required": False, "type": "select", "options": [("short", "short"), ("long", "long")], } diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 2a5c9f0bb182c..1dabe6e607192 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -128,7 +128,12 @@ async def test_get_condition_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( @@ -158,7 +163,12 @@ async def test_get_condition_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index ae54bbd25123d..99e0a5e5b93b2 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -133,7 +133,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -163,7 +168,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/lock/test_device_trigger.py b/tests/components/lock/test_device_trigger.py index 7d1c39d10f042..a5b8e47ef2fba 100644 --- a/tests/components/lock/test_device_trigger.py +++ b/tests/components/lock/test_device_trigger.py @@ -155,7 +155,12 @@ async def test_get_trigger_capabilities( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -187,7 +192,12 @@ async def test_get_trigger_capabilities_legacy( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } diff --git a/tests/components/media_player/test_device_trigger.py b/tests/components/media_player/test_device_trigger.py index ae3a84e66a0ca..e82f1cd361248 100644 --- a/tests/components/media_player/test_device_trigger.py +++ b/tests/components/media_player/test_device_trigger.py @@ -161,7 +161,12 @@ async def test_get_trigger_capabilities( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -193,7 +198,12 @@ async def test_get_trigger_capabilities_legacy( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index b4dd513c317aa..c5ba1c77c3191 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -125,7 +125,12 @@ async def test_get_condition_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( @@ -155,7 +160,12 @@ async def test_get_condition_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 800d090fd7b76..0321ba8bbaafa 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -125,7 +125,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -155,7 +160,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/select/test_device_action.py b/tests/components/select/test_device_action.py index 0ffb860179d06..446dbd0a0bf3b 100644 --- a/tests/components/select/test_device_action.py +++ b/tests/components/select/test_device_action.py @@ -376,6 +376,7 @@ async def test_get_action_capabilities( { "name": "cycle", "optional": True, + "required": False, "type": "boolean", "default": True, }, @@ -391,6 +392,7 @@ async def test_get_action_capabilities( { "name": "cycle", "optional": True, + "required": False, "type": "boolean", "default": True, }, @@ -476,6 +478,7 @@ async def test_get_action_capabilities_legacy( { "name": "cycle", "optional": True, + "required": False, "type": "boolean", "default": True, }, @@ -491,6 +494,7 @@ async def test_get_action_capabilities_legacy( { "name": "cycle", "optional": True, + "required": False, "type": "boolean", "default": True, }, diff --git a/tests/components/select/test_device_condition.py b/tests/components/select/test_device_condition.py index fc35757fa671b..8378870187725 100644 --- a/tests/components/select/test_device_condition.py +++ b/tests/components/select/test_device_condition.py @@ -276,6 +276,7 @@ async def test_get_condition_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -301,6 +302,7 @@ async def test_get_condition_capabilities( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -336,6 +338,7 @@ async def test_get_condition_capabilities_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -361,6 +364,7 @@ async def test_get_condition_capabilities_legacy( { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] diff --git a/tests/components/select/test_device_trigger.py b/tests/components/select/test_device_trigger.py index dbb4e23d78572..1d8f8b07ad7c3 100644 --- a/tests/components/select/test_device_trigger.py +++ b/tests/components/select/test_device_trigger.py @@ -310,18 +310,21 @@ async def test_get_trigger_capabilities( { "name": "from", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "to", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -341,18 +344,21 @@ async def test_get_trigger_capabilities( { "name": "from", "optional": True, + "required": False, "type": "select", "options": [("option1", "option1"), ("option2", "option2")], }, { "name": "to", "optional": True, + "required": False, "type": "select", "options": [("option1", "option1"), ("option2", "option2")], }, { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -380,18 +386,21 @@ async def test_get_trigger_capabilities_unknown( { "name": "from", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "to", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -421,18 +430,21 @@ async def test_get_trigger_capabilities_legacy( { "name": "from", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "to", "optional": True, + "required": False, "type": "select", "options": [], }, { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] @@ -452,18 +464,21 @@ async def test_get_trigger_capabilities_legacy( { "name": "from", "optional": True, + "required": False, "type": "select", "options": [("option1", "option1"), ("option2", "option2")], }, { "name": "to", "optional": True, + "required": False, "type": "select", "options": [("option1", "option1"), ("option2", "option2")], }, { "name": "for", "optional": True, + "required": False, "type": "positive_time_period_dict", }, ] diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index da69610f4c5dc..88bec54c93677 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -329,12 +329,14 @@ async def test_get_condition_capabilities( "description": {"suffix": PERCENTAGE}, "name": "above", "optional": True, + "required": False, "type": "float", }, { "description": {"suffix": PERCENTAGE}, "name": "below", "optional": True, + "required": False, "type": "float", }, ] @@ -398,12 +400,14 @@ async def test_get_condition_capabilities_legacy( "description": {"suffix": PERCENTAGE}, "name": "above", "optional": True, + "required": False, "type": "float", }, { "description": {"suffix": PERCENTAGE}, "name": "below", "optional": True, + "required": False, "type": "float", }, ] diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index c39a5216f0f8f..31bd0d2be550d 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -277,15 +277,22 @@ async def test_get_trigger_capabilities( "description": {"suffix": PERCENTAGE}, "name": "above", "optional": True, + "required": False, "type": "float", }, { "description": {"suffix": PERCENTAGE}, "name": "below", "optional": True, + "required": False, "type": "float", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] } triggers = await async_get_device_automations( @@ -347,15 +354,22 @@ async def test_get_trigger_capabilities_legacy( "description": {"suffix": PERCENTAGE}, "name": "above", "optional": True, + "required": False, "type": "float", }, { "description": {"suffix": PERCENTAGE}, "name": "below", "optional": True, + "required": False, "type": "float", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] } triggers = await async_get_device_automations( diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index 5c5737804e14b..89c84b1ed346a 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -125,7 +125,12 @@ async def test_get_condition_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( @@ -155,7 +160,12 @@ async def test_get_condition_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } conditions = await async_get_device_automations( diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index 81f8a93611db4..a642bb44825fe 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -125,7 +125,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -155,7 +160,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/update/test_device_trigger.py b/tests/components/update/test_device_trigger.py index 55138430ca04a..7cdaf4ca720cb 100644 --- a/tests/components/update/test_device_trigger.py +++ b/tests/components/update/test_device_trigger.py @@ -127,7 +127,12 @@ async def test_get_trigger_capabilities( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( @@ -157,7 +162,12 @@ async def test_get_trigger_capabilities_legacy( ) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } triggers = await async_get_device_automations( diff --git a/tests/components/vacuum/test_device_trigger.py b/tests/components/vacuum/test_device_trigger.py index 381cc1caa475c..330f14be5076d 100644 --- a/tests/components/vacuum/test_device_trigger.py +++ b/tests/components/vacuum/test_device_trigger.py @@ -134,7 +134,12 @@ async def test_get_trigger_capabilities( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } @@ -166,7 +171,12 @@ async def test_get_trigger_capabilities_legacy( ) assert capabilities == { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + } ] } diff --git a/tests/components/zha/data.py b/tests/components/zha/data.py index 80a3df524cdd7..11c09229eb84d 100644 --- a/tests/components/zha/data.py +++ b/tests/components/zha/data.py @@ -9,6 +9,7 @@ "valueMax": 6553.6, "name": "default_light_transition", "optional": True, + "required": False, "default": 0, }, { @@ -40,6 +41,7 @@ "valueMin": 0, "name": "consider_unavailable_mains", "optional": True, + "required": False, "default": 7200, }, { @@ -47,6 +49,7 @@ "valueMin": 0, "name": "consider_unavailable_battery", "optional": True, + "required": False, "default": 21600, }, { @@ -80,6 +83,7 @@ "valueMax": 6553.6, "name": "default_light_transition", "optional": True, + "required": False, "default": 0, }, { @@ -111,6 +115,7 @@ "valueMin": 0, "name": "consider_unavailable_mains", "optional": True, + "required": False, "default": 7200, }, { @@ -118,6 +123,7 @@ "valueMin": 0, "name": "consider_unavailable_battery", "optional": True, + "required": False, "default": 21600, }, { diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index f52b403869ecd..9c833cde0be31 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -71,12 +71,14 @@ async def test_zcl_schema_conversions(hass: HomeAssistant) -> None: "options": ["Execute if off present"], "name": "options_mask", "optional": True, + "required": False, }, { "type": "multi_select", "options": ["Execute if off"], "name": "options_override", "optional": True, + "required": False, }, ] vol_schema = voluptuous_serialize.convert( diff --git a/tests/components/zwave_js/test_device_action.py b/tests/components/zwave_js/test_device_action.py index 46686be0994a3..fec75d1c0327a 100644 --- a/tests/components/zwave_js/test_device_action.py +++ b/tests/components/zwave_js/test_device_action.py @@ -549,7 +549,14 @@ async def test_get_action_capabilities( assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"type": "boolean", "name": "refresh_all_values", "optional": True}] + ) == [ + { + "type": "boolean", + "name": "refresh_all_values", + "optional": True, + "required": False, + } + ] # Test ping capabilities = await device_action.async_get_action_capabilities( @@ -608,10 +615,15 @@ async def test_get_action_capabilities( "type": "select", }, {"name": "property", "required": True, "type": "string"}, - {"name": "property_key", "optional": True, "type": "string"}, - {"name": "endpoint", "optional": True, "type": "string"}, + {"name": "property_key", "optional": True, "required": False, "type": "string"}, + {"name": "endpoint", "optional": True, "required": False, "type": "string"}, {"name": "value", "required": True, "type": "string"}, - {"type": "boolean", "name": "wait_for_result", "optional": True}, + { + "type": "boolean", + "name": "wait_for_result", + "optional": True, + "required": False, + }, ] # Test enumerated type param @@ -771,7 +783,7 @@ async def test_get_action_capabilities_meter_triggers( assert voluptuous_serialize.convert( capabilities["extra_fields"], custom_serializer=cv.custom_serializer - ) == [{"type": "string", "name": "value", "optional": True}] + ) == [{"type": "string", "name": "value", "optional": True, "required": False}] async def test_failure_scenarios( diff --git a/tests/components/zwave_js/test_device_condition.py b/tests/components/zwave_js/test_device_condition.py index 17bc4cf0f5d51..123191e1f3ab9 100644 --- a/tests/components/zwave_js/test_device_condition.py +++ b/tests/components/zwave_js/test_device_condition.py @@ -511,8 +511,8 @@ async def test_get_condition_capabilities_value( "type": "select", }, {"name": "property", "required": True, "type": "string"}, - {"name": "property_key", "optional": True, "type": "string"}, - {"name": "endpoint", "optional": True, "type": "string"}, + {"name": "property_key", "optional": True, "required": False, "type": "string"}, + {"name": "endpoint", "optional": True, "required": False, "type": "string"}, {"name": "value", "required": True, "type": "string"}, ] diff --git a/tests/components/zwave_js/test_device_trigger.py b/tests/components/zwave_js/test_device_trigger.py index ccc69f7723d4b..7ff76888ce451 100644 --- a/tests/components/zwave_js/test_device_trigger.py +++ b/tests/components/zwave_js/test_device_trigger.py @@ -201,10 +201,15 @@ async def test_get_trigger_capabilities_notification_notification( capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == unordered( [ - {"name": "type.", "optional": True, "type": "string"}, - {"name": "label", "optional": True, "type": "string"}, - {"name": "event", "optional": True, "type": "string"}, - {"name": "event_label", "optional": True, "type": "string"}, + {"name": "type.", "optional": True, "required": False, "type": "string"}, + {"name": "label", "optional": True, "required": False, "type": "string"}, + {"name": "event", "optional": True, "required": False, "type": "string"}, + { + "name": "event_label", + "optional": True, + "required": False, + "type": "string", + }, ] ) @@ -336,8 +341,18 @@ async def test_get_trigger_capabilities_entry_control_notification( capabilities["extra_fields"], custom_serializer=cv.custom_serializer ) == unordered( [ - {"name": "event_type", "optional": True, "type": "string"}, - {"name": "data_type", "optional": True, "type": "string"}, + { + "name": "event_type", + "optional": True, + "required": False, + "type": "string", + }, + { + "name": "data_type", + "optional": True, + "required": False, + "type": "string", + }, ] ) @@ -580,6 +595,7 @@ async def test_get_trigger_capabilities_node_status( { "name": "from", "optional": True, + "required": False, "options": [ ("asleep", "asleep"), ("awake", "awake"), @@ -591,6 +607,7 @@ async def test_get_trigger_capabilities_node_status( { "name": "to", "optional": True, + "required": False, "options": [ ("asleep", "asleep"), ("awake", "awake"), @@ -599,7 +616,12 @@ async def test_get_trigger_capabilities_node_status( ], "type": "select", }, - {"name": "for", "optional": True, "type": "positive_time_period_dict"}, + { + "name": "for", + "optional": True, + "required": False, + "type": "positive_time_period_dict", + }, ] @@ -781,6 +803,7 @@ async def test_get_trigger_capabilities_basic_value_notification( { "name": "value", "optional": True, + "required": False, "type": "integer", "valueMin": 0, "valueMax": 255, @@ -972,6 +995,7 @@ async def test_get_trigger_capabilities_central_scene_value_notification( { "name": "value", "optional": True, + "required": False, "type": "select", "options": [(0, "KeyPressed"), (1, "KeyReleased"), (2, "KeyHeldDown")], }, @@ -1156,6 +1180,7 @@ async def test_get_trigger_capabilities_scene_activation_value_notification( { "name": "value", "optional": True, + "required": False, "type": "integer", "valueMin": 1, "valueMax": 255, @@ -1406,10 +1431,10 @@ async def test_get_trigger_capabilities_value_updated_value( ], }, {"name": "property", "required": True, "type": "string"}, - {"name": "property_key", "optional": True, "type": "string"}, - {"name": "endpoint", "optional": True, "type": "string"}, - {"name": "from", "optional": True, "type": "string"}, - {"name": "to", "optional": True, "type": "string"}, + {"name": "property_key", "optional": True, "required": False, "type": "string"}, + {"name": "endpoint", "optional": True, "required": False, "type": "string"}, + {"name": "from", "optional": True, "required": False, "type": "string"}, + {"name": "to", "optional": True, "required": False, "type": "string"}, ] @@ -1552,6 +1577,7 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_range( { "name": "from", "optional": True, + "required": False, "valueMin": 0, "valueMax": 255, "type": "integer", @@ -1559,6 +1585,7 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_range( { "name": "to", "optional": True, + "required": False, "valueMin": 0, "valueMax": 255, "type": "integer", @@ -1600,12 +1627,14 @@ async def test_get_trigger_capabilities_value_updated_config_parameter_enumerate { "name": "from", "optional": True, + "required": False, "options": [(0, "Disable Beeper"), (255, "Enable Beeper")], "type": "select", }, { "name": "to", "optional": True, + "required": False, "options": [(0, "Disable Beeper"), (255, "Enable Beeper")], "type": "select", }, diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index 0faa4dd1a80aa..f0912188b9e95 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -1229,7 +1229,13 @@ def test_section_in_serializer() -> None: ) == { "expanded": True, "schema": [ - {"default": False, "name": "option_1", "optional": True, "type": "boolean"}, + { + "default": False, + "name": "option_1", + "optional": True, + "required": False, + "type": "boolean", + }, {"name": "option_2", "required": True, "type": "integer"}, ], "type": "expandable", From 22909406385c6620f31f81fcdf1a7cdc1da2b203 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 19 Aug 2025 16:54:05 +0200 Subject: [PATCH 18/23] Modbus: Remove unused variable. (#150894) --- homeassistant/components/modbus/modbus.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index 5758762305871..f8604efdc2fff 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -253,7 +253,6 @@ def __init__(self, hass: HomeAssistant, client_config: dict[str, Any]) -> None: self._client: ( AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None ) = None - self._in_error = False self._lock = asyncio.Lock() self.event_connected = asyncio.Event() self.hass = hass @@ -408,7 +407,6 @@ async def low_level_pb_call( error = f"Error: device: {slave} address: {address} -> pymodbus returned isError True" self._log_error(error) return None - self._in_error = False return result async def async_pb_call( From 08fc2ab03bb47e1614cf88c11dd5a94f04d00b6a Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 19 Aug 2025 16:55:16 +0200 Subject: [PATCH 19/23] Add missing unsupported reasons to list (#150866) --- homeassistant/components/hassio/issues.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 68d01e93bebfa..22406e86ba118 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -61,18 +61,19 @@ UNSUPPORTED_REASONS = { "apparmor", + "cgroup_version", "connectivity_check", "content_trust", "dbus", "dns_server", "docker_configuration", "docker_version", - "cgroup_version", "job_conditions", "lxc", "network_manager", "os", "os_agent", + "os_version", "restart_policy", "software", "source_mods", @@ -80,6 +81,7 @@ "systemd", "systemd_journal", "systemd_resolved", + "virtualization_image", } # Some unsupported reasons also mark the system as unhealthy. If the unsupported reason # provides no additional information beyond the unhealthy one then skip that repair. From 65696f9b53bff524412c1d209154b2f3429e035d Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 19 Aug 2025 16:55:36 +0200 Subject: [PATCH 20/23] Bump aiohasupervisor from version 0.3.1 to version 0.3.2b0 (#150893) --- homeassistant/components/hassio/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index a2af6fb217cb3..34a8f466158ed 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/hassio", "iot_class": "local_polling", "quality_scale": "internal", - "requirements": ["aiohasupervisor==0.3.1"], + "requirements": ["aiohasupervisor==0.3.2b0"], "single_config_entry": true } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4dfb2dad73474..764cb8357179b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -3,7 +3,7 @@ aiodhcpwatcher==1.2.1 aiodiscover==2.7.1 aiodns==3.5.0 -aiohasupervisor==0.3.1 +aiohasupervisor==0.3.2b0 aiohttp-asyncmdnsresolver==0.1.1 aiohttp-fast-zlib==0.3.0 aiohttp==3.12.15 diff --git a/pyproject.toml b/pyproject.toml index 30860e381d354..343d581577b34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ # Integrations may depend on hassio integration without listing it to # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 - "aiohasupervisor==0.3.1", + "aiohasupervisor==0.3.2b0", "aiohttp==3.12.15", "aiohttp_cors==0.8.1", "aiohttp-fast-zlib==0.3.0", diff --git a/requirements.txt b/requirements.txt index 143df76e63692..e94f0c3caea9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ # Home Assistant Core aiodns==3.5.0 -aiohasupervisor==0.3.1 +aiohasupervisor==0.3.2b0 aiohttp==3.12.15 aiohttp_cors==0.8.1 aiohttp-fast-zlib==0.3.0 diff --git a/requirements_all.txt b/requirements_all.txt index de446ac232251..37b4185689f98 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -265,7 +265,7 @@ aioguardian==2022.07.0 aioharmony==0.5.2 # homeassistant.components.hassio -aiohasupervisor==0.3.1 +aiohasupervisor==0.3.2b0 # homeassistant.components.home_connect aiohomeconnect==0.18.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3fd4773dffd35..c1c992a640348 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -250,7 +250,7 @@ aioguardian==2022.07.0 aioharmony==0.5.2 # homeassistant.components.hassio -aiohasupervisor==0.3.1 +aiohasupervisor==0.3.2b0 # homeassistant.components.home_connect aiohomeconnect==0.18.1 From 10fe4793113e48e993e51be07f0a3265a829b2d1 Mon Sep 17 00:00:00 2001 From: rossfoss <206272443+rossfoss@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:04:35 +0100 Subject: [PATCH 21/23] =?UTF-8?q?Add=20new=20attributes=20to=20Met=20?= =?UTF-8?q?=C3=89ireann=20(#150653)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joost Lekkerkerker --- homeassistant/components/met_eireann/const.py | 6 ++++++ homeassistant/components/met_eireann/weather.py | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/homeassistant/components/met_eireann/const.py b/homeassistant/components/met_eireann/const.py index 3a4c3dda50783..1d2bf456c9d0b 100644 --- a/homeassistant/components/met_eireann/const.py +++ b/homeassistant/components/met_eireann/const.py @@ -10,9 +10,12 @@ ATTR_CONDITION_SNOWY, ATTR_CONDITION_SNOWY_RAINY, ATTR_CONDITION_SUNNY, + ATTR_FORECAST_CLOUD_COVERAGE, + ATTR_FORECAST_HUMIDITY, ATTR_FORECAST_NATIVE_PRESSURE, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_WIND_BEARING, @@ -34,6 +37,9 @@ ATTR_FORECAST_NATIVE_TEMP_LOW: "templow", ATTR_FORECAST_WIND_BEARING: "wind_bearing", ATTR_FORECAST_NATIVE_WIND_SPEED: "wind_speed", + ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: "wind_gust", + ATTR_FORECAST_CLOUD_COVERAGE: "cloudiness", + ATTR_FORECAST_HUMIDITY: "humidity", } CONDITION_MAP = { diff --git a/homeassistant/components/met_eireann/weather.py b/homeassistant/components/met_eireann/weather.py index 68f46f0a65645..b6095c174f2a9 100644 --- a/homeassistant/components/met_eireann/weather.py +++ b/homeassistant/components/met_eireann/weather.py @@ -138,6 +138,16 @@ def wind_bearing(self) -> float | None: """Return the wind direction.""" return self.coordinator.data.current_weather_data.get("wind_bearing") + @property + def native_wind_gust_speed(self) -> float | None: + """Return the wind gust speed in native units.""" + return self.coordinator.data.current_weather_data.get("wind_gust") + + @property + def cloud_coverage(self) -> float | None: + """Return the cloud coverage.""" + return self.coordinator.data.current_weather_data.get("cloudiness") + def _forecast(self, hourly: bool) -> list[Forecast]: """Return the forecast array.""" if hourly: From cded1639308d968b5f0690f7dd5511d244155340 Mon Sep 17 00:00:00 2001 From: Luke Heckman <70358560+lukeheckman@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:15:31 -0400 Subject: [PATCH 22/23] Update contributing guide links (#150159) Co-authored-by: Joost Lekkerkerker --- CONTRIBUTING.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8f8a79ab90167..e7d8488048e58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,5 +14,8 @@ Still interested? Then you should take a peek at the [developer documentation](h ## Feature suggestions -If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests). -We use [GitHub for tracking issues](https://github.com/home-assistant/core/issues), not for tracking feature requests. +If you want to suggest a new feature for Home Assistant (e.g. new integrations), please [start a discussion](https://github.com/orgs/home-assistant/discussions) on GitHub. + +## Issue Tracker + +If you want to report an issue, please [create an issue](https://github.com/home-assistant/core/issues) on GitHub. From 7ecf32390cfe9203c7e1046464b71b2e098ed4cd Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 19 Aug 2025 17:15:51 +0200 Subject: [PATCH 23/23] Modbus: Avoid duplicate updates. (#150895) --- homeassistant/components/modbus/entity.py | 40 +++++++++++++---------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index cde017d4dd799..689d882a2f37b 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -3,7 +3,6 @@ from __future__ import annotations from abc import abstractmethod -import asyncio from collections.abc import Callable from datetime import datetime, timedelta import struct @@ -28,7 +27,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, ToggleEntity -from homeassistant.helpers.event import async_call_later, async_track_time_interval +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.restore_state import RestoreEntity from .const import ( @@ -108,29 +107,39 @@ def get_optional_numeric_config(config_name: str) -> int | float | None: self._max_value = get_optional_numeric_config(CONF_MAX_VALUE) self._nan_value = entry.get(CONF_NAN_VALUE) self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS) - self._update_lock = asyncio.Lock() @abstractmethod async def _async_update(self) -> None: """Virtual function to be overwritten.""" - async def async_update(self, now: datetime | None = None) -> None: + async def async_update(self) -> None: """Update the entity state.""" - async with self._update_lock: - await self._async_update() + if self._cancel_call: + self._cancel_call() + await self.async_local_update() + + async def async_local_update(self, now: datetime | None = None) -> None: + """Update the entity state.""" + await self._async_update() + self.async_write_ha_state() + if self._scan_interval > 0: + self._cancel_call = async_call_later( + self.hass, + timedelta(seconds=self._scan_interval), + self.async_local_update, + ) async def _async_update_write_state(self) -> None: """Update the entity state and write it to the state machine.""" - await self.async_update() - self.async_write_ha_state() + if self._cancel_call: + self._cancel_call() + self._cancel_call = None + await self.async_local_update() async def _async_update_if_not_in_progress( self, now: datetime | None = None ) -> None: """Update the entity state if not already in progress.""" - if self._update_lock.locked(): - _LOGGER.debug("Update for entity %s is already in progress", self.name) - return await self._async_update_write_state() @callback @@ -138,12 +147,9 @@ def async_run(self) -> None: """Remote start entity.""" self._async_cancel_update_polling() self._async_schedule_future_update(0.1) - if self._scan_interval > 0: - self._cancel_timer = async_track_time_interval( - self.hass, - self._async_update_if_not_in_progress, - timedelta(seconds=self._scan_interval), - ) + self._cancel_call = async_call_later( + self.hass, timedelta(seconds=0.1), self.async_local_update + ) self._attr_available = True self.async_write_ha_state()