From ca37bc15068c5ae5235fba6e4c5c5f1c9df258eb Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 25 Aug 2025 11:28:55 +0200 Subject: [PATCH 1/5] Add Tuya test fixtures (#151022) --- tests/components/tuya/__init__.py | 15 + .../fixtures/ckmkzq_1yyqfw4djv9eii3q.json | 59 ++ .../tuya/fixtures/cl_669wsr2w4cvinbh4.json | 138 +++++ .../tuya/fixtures/cz_AiHXxAyyn7eAkLQY.json | 54 ++ .../tuya/fixtures/cz_yncyws7tu1q4cpsz.json | 37 ++ .../components/tuya/fixtures/dj_8ugheslg.json | 57 ++ .../tuya/fixtures/dj_h4aX2JkHZNByQ4AV.json | 54 ++ .../tuya/fixtures/dj_kgaob37tz2muf3mi.json | 548 ++++++++++++++++++ .../tuya/fixtures/mzj_jlapoy5liocmtdvd.json | 21 + .../tuya/fixtures/sfkzq_d4vpmigg.json | 130 +++++ .../tuya/fixtures/sfkzq_nxquc5lb.json | 130 +++++ .../tuya/fixtures/sgbj_im2eqqhj72suwwko.json | 92 +++ .../tuya/fixtures/sj_rzeSU2h9uoklxEwq.json | 41 ++ .../tuya/fixtures/wg2_2gowdgni.json | 77 +++ .../tuya/fixtures/wg2_tmwhss6ntjfc7prs.json | 21 + .../tuya/fixtures/wnykq_om518smspsaltzdi.json | 21 + .../tuya/snapshots/test_binary_sensor.ambr | 98 ++++ .../components/tuya/snapshots/test_cover.ambr | 101 ++++ .../components/tuya/snapshots/test_init.ambr | 465 +++++++++++++++ .../components/tuya/snapshots/test_light.ambr | 193 ++++++ .../tuya/snapshots/test_number.ambr | 58 ++ .../tuya/snapshots/test_select.ambr | 299 ++++++++++ .../tuya/snapshots/test_sensor.ambr | 312 ++++++++++ .../tuya/snapshots/test_switch.ambr | 193 ++++++ 24 files changed, 3214 insertions(+) create mode 100644 tests/components/tuya/fixtures/ckmkzq_1yyqfw4djv9eii3q.json create mode 100644 tests/components/tuya/fixtures/cl_669wsr2w4cvinbh4.json create mode 100644 tests/components/tuya/fixtures/cz_AiHXxAyyn7eAkLQY.json create mode 100644 tests/components/tuya/fixtures/cz_yncyws7tu1q4cpsz.json create mode 100644 tests/components/tuya/fixtures/dj_8ugheslg.json create mode 100644 tests/components/tuya/fixtures/dj_h4aX2JkHZNByQ4AV.json create mode 100644 tests/components/tuya/fixtures/dj_kgaob37tz2muf3mi.json create mode 100644 tests/components/tuya/fixtures/mzj_jlapoy5liocmtdvd.json create mode 100644 tests/components/tuya/fixtures/sfkzq_d4vpmigg.json create mode 100644 tests/components/tuya/fixtures/sfkzq_nxquc5lb.json create mode 100644 tests/components/tuya/fixtures/sgbj_im2eqqhj72suwwko.json create mode 100644 tests/components/tuya/fixtures/sj_rzeSU2h9uoklxEwq.json create mode 100644 tests/components/tuya/fixtures/wg2_2gowdgni.json create mode 100644 tests/components/tuya/fixtures/wg2_tmwhss6ntjfc7prs.json create mode 100644 tests/components/tuya/fixtures/wnykq_om518smspsaltzdi.json diff --git a/tests/components/tuya/__init__.py b/tests/components/tuya/__init__.py index cea7d633245d84..1fdc28bcb9f1b7 100644 --- a/tests/components/tuya/__init__.py +++ b/tests/components/tuya/__init__.py @@ -15,7 +15,9 @@ DEVICE_MOCKS = [ "bzyd_45idzfufidgee7ir", # https://github.com/orgs/home-assistant/discussions/717 "bzyd_ssimhf6r8kgwepfb", # https://github.com/orgs/home-assistant/discussions/718 + "ckmkzq_1yyqfw4djv9eii3q", # https://github.com/home-assistant/core/issues/150856 "cl_3r8gc33pnqsxfe1g", # https://github.com/tuya/tuya-home-assistant/issues/754 + "cl_669wsr2w4cvinbh4", # https://github.com/home-assistant/core/issues/150856 "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 @@ -40,6 +42,7 @@ "cz_39sy2g68gsjwo2xv", # https://github.com/home-assistant/core/issues/141278 "cz_6fa7odsufen374x2", # https://github.com/home-assistant/core/issues/150029 "cz_9ivirni8wemum6cw", # https://github.com/home-assistant/core/issues/139735 + "cz_AiHXxAyyn7eAkLQY", # https://github.com/home-assistant/core/issues/150662 "cz_CHLZe9HQ6QIXujVN", # https://github.com/home-assistant/core/issues/149233 "cz_HBRBzv1UVBVfF6SL", # https://github.com/tuya/tuya-home-assistant/issues/754 "cz_anwgf2xugjxpkfxb", # https://github.com/orgs/home-assistant/discussions/539 @@ -68,11 +71,13 @@ "cz_wifvoilfrqeo6hvu", # https://github.com/home-assistant/core/issues/146164 "cz_wrz6vzch8htux2zp", # https://github.com/home-assistant/core/issues/141278 "cz_y4jnobxh", # https://github.com/orgs/home-assistant/discussions/482 + "cz_yncyws7tu1q4cpsz", # https://github.com/home-assistant/core/issues/150662 "cz_z6pht25s3p0gs26q", # https://github.com/home-assistant/core/issues/63978 "dc_l3bpgg8ibsagon4x", # https://github.com/home-assistant/core/issues/149704 "dd_gaobbrxqiblcng2p", # https://github.com/home-assistant/core/issues/149233 "dj_0gyaslysqfp4gfis", # https://github.com/home-assistant/core/issues/149895 "dj_8szt7whdvwpmxglk", # https://github.com/home-assistant/core/issues/149704 + "dj_8ugheslg", # https://github.com/home-assistant/core/issues/150856 "dj_8y0aquaa8v6tho8w", # https://github.com/home-assistant/core/issues/149704 "dj_AqHUMdcbYzIq1Of4", # https://github.com/orgs/home-assistant/discussions/539 "dj_amx1bgdrfab6jngb", # https://github.com/orgs/home-assistant/discussions/482 @@ -84,12 +89,14 @@ "dj_djnozmdyqyriow8z", # https://github.com/home-assistant/core/issues/149704 "dj_ekwolitfjhxn55js", # https://github.com/home-assistant/core/issues/149704 "dj_fuupmcr2mb1odkja", # https://github.com/home-assistant/core/issues/149704 + "dj_h4aX2JkHZNByQ4AV", # https://github.com/home-assistant/core/issues/150662 "dj_hp6orhaqm6as3jnv", # https://github.com/home-assistant/core/issues/149704 "dj_hpc8ddyfv85haxa7", # https://github.com/home-assistant/core/issues/149704 "dj_iayz2jmtlipjnxj7", # https://github.com/home-assistant/core/issues/149704 "dj_idnfq7xbx8qewyoa", # https://github.com/home-assistant/core/issues/149704 "dj_ilddqqih3tucdk68", # https://github.com/home-assistant/core/issues/149704 "dj_j1bgp31cffutizub", # https://github.com/home-assistant/core/issues/149704 + "dj_kgaob37tz2muf3mi", # https://github.com/home-assistant/core/issues/150856 "dj_lmnt3uyltk1xffrt", # https://github.com/home-assistant/core/issues/149704 "dj_mki13ie507rlry4r", # https://github.com/home-assistant/core/pull/126242 "dj_nbumqpv8vz61enji", # https://github.com/home-assistant/core/issues/149704 @@ -140,6 +147,7 @@ "mcs_8yhypbo7", # https://github.com/orgs/home-assistant/discussions/482 "mcs_hx5ztlztij4yxxvg", # https://github.com/home-assistant/core/issues/148347 "mcs_qxu3flpqjsc1kqu3", # https://github.com/home-assistant/core/issues/141278 + "mzj_jlapoy5liocmtdvd", # https://github.com/home-assistant/core/issues/150662 "mzj_qavcakohisj5adyh", # https://github.com/home-assistant/core/issues/141278 "ntq_9mqdhwklpvnnvb7t", # https://github.com/orgs/home-assistant/discussions/517 "pc_t2afic7i3v1bwhfp", # https://github.com/home-assistant/core/issues/149704 @@ -157,10 +165,14 @@ "sd_i6hyjg3af7doaswm", # https://github.com/orgs/home-assistant/discussions/539 "sd_lr33znaodtyarrrz", # https://github.com/home-assistant/core/issues/141278 "sfkzq_1fcnd8xk", # https://github.com/orgs/home-assistant/discussions/539 + "sfkzq_d4vpmigg", # https://github.com/home-assistant/core/issues/150662 "sfkzq_ed7frwissyqrejic", # https://github.com/home-assistant/core/pull/149236 + "sfkzq_nxquc5lb", # https://github.com/home-assistant/core/issues/150662 "sfkzq_o6dagifntoafakst", # https://github.com/home-assistant/core/issues/148116 "sfkzq_rzklytdei8i8vo37", # https://github.com/home-assistant/core/issues/146164 + "sgbj_im2eqqhj72suwwko", # https://github.com/home-assistant/core/issues/151082 "sgbj_ulv4nnue7gqp0rjk", # https://github.com/home-assistant/core/issues/149704 + "sj_rzeSU2h9uoklxEwq", # https://github.com/home-assistant/core/issues/150683 "sj_tgvtvdoc", # https://github.com/orgs/home-assistant/discussions/482 "sp_drezasavompxpcgm", # https://github.com/home-assistant/core/issues/149704 "sp_nzauwyj3mcnjnf35", # https://github.com/home-assistant/core/issues/141278 @@ -176,9 +188,11 @@ "tyndj_pyakuuoc", # https://github.com/home-assistant/core/issues/149704 "wfcon_b25mh8sxawsgndck", # https://github.com/home-assistant/core/issues/149704 "wfcon_lieerjyy6l4ykjor", # https://github.com/home-assistant/core/issues/136055 + "wg2_2gowdgni", # https://github.com/home-assistant/core/issues/150856 "wg2_haclbl0qkqlf2qds", # https://github.com/orgs/home-assistant/discussions/517 "wg2_nwxr8qcu4seltoro", # https://github.com/orgs/home-assistant/discussions/430 "wg2_setmxeqgs63xwopm", # https://github.com/orgs/home-assistant/discussions/539 + "wg2_tmwhss6ntjfc7prs", # https://github.com/home-assistant/core/issues/150662 "wg2_v7owd9tzcaninc36", # https://github.com/orgs/home-assistant/discussions/539 "wk_6kijc7nd", # https://github.com/home-assistant/core/issues/136513 "wk_aqoouq7x", # https://github.com/home-assistant/core/issues/146263 @@ -189,6 +203,7 @@ "wk_y5obtqhuztqsf2mj", # https://github.com/home-assistant/core/issues/139735 "wkcz_gc4b1mdw7kebtuyz", # https://github.com/home-assistant/core/issues/135617 "wnykq_npbbca46yiug8ysk", # https://github.com/orgs/home-assistant/discussions/539 + "wnykq_om518smspsaltzdi", # https://github.com/home-assistant/core/issues/150662 "wnykq_rqhxdyusjrwxyff6", # https://github.com/home-assistant/core/issues/133173 "wsdcg_g2y6z3p3ja2qhyav", # https://github.com/home-assistant/core/issues/102769 "wsdcg_iq4ygaai", # https://github.com/orgs/home-assistant/discussions/482 diff --git a/tests/components/tuya/fixtures/ckmkzq_1yyqfw4djv9eii3q.json b/tests/components/tuya/fixtures/ckmkzq_1yyqfw4djv9eii3q.json new file mode 100644 index 00000000000000..ba3bb5b2cf36d9 --- /dev/null +++ b/tests/components/tuya/fixtures/ckmkzq_1yyqfw4djv9eii3q.json @@ -0,0 +1,59 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Garage door ", + "category": "ckmkzq", + "product_id": "1yyqfw4djv9eii3q", + "product_name": "Garage door ", + "online": true, + "sub": false, + "time_zone": "-05:00", + "active_time": "2024-07-31T17:45:05+00:00", + "create_time": "2024-07-31T17:45:05+00:00", + "update_time": "2024-07-31T17:45:05+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "doorcontact_state": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_1": false, + "countdown_1": 0, + "doorcontact_state": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cl_669wsr2w4cvinbh4.json b/tests/components/tuya/fixtures/cl_669wsr2w4cvinbh4.json new file mode 100644 index 00000000000000..de20e242236a81 --- /dev/null +++ b/tests/components/tuya/fixtures/cl_669wsr2w4cvinbh4.json @@ -0,0 +1,138 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "VIVIDSTORM SCREEN", + "category": "cl", + "product_id": "669wsr2w4cvinbh4", + "product_name": "VIVIDSTORM SCREEN", + "online": true, + "sub": false, + "time_zone": "-05:00", + "active_time": "2025-08-15T17:34:27+00:00", + "create_time": "2025-08-15T17:34:27+00:00", + "update_time": "2025-08-15T17:34:27+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", "down_delete", "remove_top_bottom"] + } + } + }, + "status_range": { + "control": { + "type": "Enum", + "value": { + "range": ["open", "stop", "close", "continue"] + } + }, + "percent_control": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "percent_state": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "control_back_mode": { + "type": "Enum", + "value": { + "range": ["forward", "back"] + } + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["opening", "closing"] + } + }, + "countdown_left": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "time_total": { + "type": "Integer", + "value": { + "unit": "ms", + "min": 0, + "max": 120000, + "scale": 0, + "step": 1 + } + }, + "situation_set": { + "type": "Enum", + "value": { + "range": ["fully_open", "fully_close"] + } + }, + "fault": { + "type": "Bitmap", + "value": { + "label": ["motor_fault"] + } + }, + "border": { + "type": "Enum", + "value": { + "range": ["up", "down", "up_delete", "down_delete", "remove_top_bottom"] + } + } + }, + "status": { + "control": "open", + "percent_control": 0, + "percent_state": 0, + "control_back_mode": "forward", + "work_state": "opening", + "countdown_left": 0, + "time_total": 0, + "situation_set": "fully_open", + "fault": 0, + "border": "down" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_AiHXxAyyn7eAkLQY.json b/tests/components/tuya/fixtures/cz_AiHXxAyyn7eAkLQY.json new file mode 100644 index 00000000000000..67510bc7ec38cf --- /dev/null +++ b/tests/components/tuya/fixtures/cz_AiHXxAyyn7eAkLQY.json @@ -0,0 +1,54 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Solar Heater Pump", + "category": "cz", + "product_id": "AiHXxAyyn7eAkLQY", + "product_name": "Mini Smart Plug", + "online": true, + "sub": false, + "time_zone": "-05:00", + "active_time": "2021-07-13T21:37:26+00:00", + "create_time": "2021-07-13T21:37:26+00:00", + "update_time": "2021-07-13T21:37:26+00:00", + "function": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_1": { + "type": "Boolean", + "value": {} + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch_1": true, + "countdown_1": 0 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/cz_yncyws7tu1q4cpsz.json b/tests/components/tuya/fixtures/cz_yncyws7tu1q4cpsz.json new file mode 100644 index 00000000000000..c0cfa202a5017b --- /dev/null +++ b/tests/components/tuya/fixtures/cz_yncyws7tu1q4cpsz.json @@ -0,0 +1,37 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Wi-Fi hub", + "category": "cz", + "product_id": "yncyws7tu1q4cpsz", + "product_name": "Wi-Fi hub", + "online": true, + "sub": true, + "time_zone": "-04:00", + "active_time": "2025-08-09T18:54:03+00:00", + "create_time": "2025-08-09T18:54:03+00:00", + "update_time": "2025-08-09T18:54:03+00:00", + "function": { + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + } + }, + "status_range": { + "relay_status": { + "type": "Enum", + "value": { + "range": ["power_off", "power_on", "last"] + } + } + }, + "status": { + "relay_status": "power_on" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_8ugheslg.json b/tests/components/tuya/fixtures/dj_8ugheslg.json new file mode 100644 index 00000000000000..870618789c21f0 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_8ugheslg.json @@ -0,0 +1,57 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "POWERASIA R2", + "category": "dj", + "product_id": "8ugheslg", + "product_name": "POWERASIA", + "online": true, + "sub": true, + "time_zone": "-05:00", + "active_time": "2024-07-27T23:47:47+00:00", + "create_time": "2024-07-27T23:47:47+00:00", + "update_time": "2024-07-27T23:47:47+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "switch_led": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_h4aX2JkHZNByQ4AV.json b/tests/components/tuya/fixtures/dj_h4aX2JkHZNByQ4AV.json new file mode 100644 index 00000000000000..0f790ecfc34e28 --- /dev/null +++ b/tests/components/tuya/fixtures/dj_h4aX2JkHZNByQ4AV.json @@ -0,0 +1,54 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Entry Stairs", + "category": "dj", + "product_id": "h4aX2JkHZNByQ4AV", + "product_name": "Smart Dimmer Switch", + "online": false, + "sub": false, + "time_zone": "-05:00", + "active_time": "2023-04-25T13:21:00+00:00", + "create_time": "2023-04-25T13:21:00+00:00", + "update_time": "2023-04-25T13:21:00+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "bright_value": { + "type": "Integer", + "value": { + "min": 25, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "status": { + "switch_led": false, + "bright_value": 64 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/dj_kgaob37tz2muf3mi.json b/tests/components/tuya/fixtures/dj_kgaob37tz2muf3mi.json new file mode 100644 index 00000000000000..36a2721c58ea5f --- /dev/null +++ b/tests/components/tuya/fixtures/dj_kgaob37tz2muf3mi.json @@ -0,0 +1,548 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Parker Ceiling Fan 1", + "category": "dj", + "product_id": "kgaob37tz2muf3mi", + "product_name": "", + "online": false, + "sub": false, + "time_zone": "-05:00", + "active_time": "2024-04-05T01:27:07+00:00", + "create_time": "2024-04-05T01:27:07+00:00", + "update_time": "2024-04-05T01:27:07+00:00", + "function": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": 255 + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status_range": { + "switch_led": { + "type": "Boolean", + "value": {} + }, + "work_mode": { + "type": "Enum", + "value": { + "range": ["white", "colour", "scene", "music"] + } + }, + "bright_value_v2": { + "type": "Integer", + "value": { + "min": 10, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "temp_value_v2": { + "type": "Integer", + "value": { + "min": 0, + "max": 1000, + "scale": 0, + "step": 1 + } + }, + "colour_data_v2": { + "type": "Json", + "value": { + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + }, + "scene_data_v2": { + "type": "Json", + "value": { + "scene_num": { + "min": 1, + "scale": 0, + "max": 8, + "step": 1 + }, + "scene_units": { + "unit_change_mode": { + "range": ["static", "jump", "gradient"] + }, + "unit_switch_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "unit_gradient_duration": { + "min": 0, + "scale": 0, + "max": 100, + "step": 1 + }, + "bright": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + } + } + } + }, + "countdown_1": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "music_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "control_data": { + "type": "Json", + "value": { + "change_mode": { + "range": ["direct", "gradient"] + }, + "bright": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "temperature": { + "min": 0, + "scale": 0, + "unit": "", + "max": 1000, + "step": 1 + }, + "h": { + "min": 0, + "scale": 0, + "unit": "", + "max": 360, + "step": 1 + }, + "s": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + }, + "v": { + "min": 0, + "scale": 0, + "unit": "", + "max": 255, + "step": 1 + } + } + }, + "rhythm_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "sleep_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "wakeup_mode": { + "type": "Raw", + "value": { + "maxlen": "255" + } + }, + "power_memory": { + "type": "Raw", + "value": {} + }, + "do_not_disturb": { + "type": "Boolean", + "value": {} + }, + "remote_switch": { + "type": "Boolean", + "value": {} + }, + "random_timing": { + "type": "Raw", + "value": {} + } + }, + "status": { + "switch_led": true, + "work_mode": "white", + "bright_value_v2": 1000, + "temp_value_v2": 300, + "colour_data_v2": { + "h": 0, + "s": 1000, + "v": 1000 + }, + "scene_data_v2": { + "scene_num": 1, + "scene_units": [ + { + "bright": 200, + "h": 0, + "s": 0, + "temperature": 0, + "unit_change_mode": "static", + "unit_gradient_duration": 13, + "unit_switch_duration": 14, + "v": 0 + } + ] + }, + "countdown_1": 0, + "music_data": "", + "control_data": "", + "rhythm_mode": "AAAAAAA=", + "sleep_mode": "AAA=", + "wakeup_mode": "AAA=", + "power_memory": "AAEAAAPoA+gD6AEs", + "do_not_disturb": false, + "remote_switch": true, + "random_timing": "AAAA" + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/mzj_jlapoy5liocmtdvd.json b/tests/components/tuya/fixtures/mzj_jlapoy5liocmtdvd.json new file mode 100644 index 00000000000000..804004a6d26598 --- /dev/null +++ b/tests/components/tuya/fixtures/mzj_jlapoy5liocmtdvd.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "ISV-100W2.0", + "category": "mzj", + "product_id": "jlapoy5liocmtdvd", + "product_name": "ISV-100W2.0", + "online": true, + "sub": false, + "time_zone": "-04:00", + "active_time": "2025-08-14T21:06:41+00:00", + "create_time": "2025-08-14T21:06:41+00:00", + "update_time": "2025-08-14T21:06:41+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sfkzq_d4vpmigg.json b/tests/components/tuya/fixtures/sfkzq_d4vpmigg.json new file mode 100644 index 00000000000000..922950a358b624 --- /dev/null +++ b/tests/components/tuya/fixtures/sfkzq_d4vpmigg.json @@ -0,0 +1,130 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Garden Valve Yard", + "category": "sfkzq", + "product_id": "d4vpmigg", + "product_name": "Valve Controller", + "online": true, + "sub": true, + "time_zone": "-04:00", + "active_time": "2025-08-09T19:01:51+00:00", + "create_time": "2025-08-09T19:01:51+00:00", + "update_time": "2025-08-09T19:01:51+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "time_use": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 2592000, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "smart_weather": { + "type": "Enum", + "value": { + "range": ["sunny", "cloudy", "rainy"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "time_use": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 2592000, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["auto", "manual", "idle"] + } + }, + "smart_weather": { + "type": "Enum", + "value": { + "range": ["sunny", "cloudy", "rainy"] + } + }, + "use_time_one": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": false, + "battery_percentage": 100, + "time_use": 38201, + "weather_delay": "cancel", + "countdown": 0, + "work_state": "idle", + "smart_weather": "sunny", + "use_time_one": 237 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sfkzq_nxquc5lb.json b/tests/components/tuya/fixtures/sfkzq_nxquc5lb.json new file mode 100644 index 00000000000000..8ec1e229b85241 --- /dev/null +++ b/tests/components/tuya/fixtures/sfkzq_nxquc5lb.json @@ -0,0 +1,130 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Smart Water Timer", + "category": "sfkzq", + "product_id": "nxquc5lb", + "product_name": "Smart Water Timer", + "online": false, + "sub": true, + "time_zone": "-04:00", + "active_time": "2025-08-08T20:15:50+00:00", + "create_time": "2025-08-08T20:15:50+00:00", + "update_time": "2025-08-08T20:15:50+00:00", + "function": { + "switch": { + "type": "Boolean", + "value": {} + }, + "time_use": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 2592000, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "smart_weather": { + "type": "Enum", + "value": { + "range": ["sunny", "cloudy", "rainy"] + } + } + }, + "status_range": { + "switch": { + "type": "Boolean", + "value": {} + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "time_use": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 2592000, + "scale": 0, + "step": 1 + } + }, + "weather_delay": { + "type": "Enum", + "value": { + "range": ["cancel", "24h", "48h", "72h"] + } + }, + "countdown": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + }, + "work_state": { + "type": "Enum", + "value": { + "range": ["auto", "manual", "idle"] + } + }, + "smart_weather": { + "type": "Enum", + "value": { + "range": ["sunny", "cloudy", "rainy"] + } + }, + "use_time_one": { + "type": "Integer", + "value": { + "unit": "s", + "min": 0, + "max": 86400, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "switch": false, + "battery_percentage": 100, + "time_use": 2, + "weather_delay": "cancel", + "countdown": 599, + "work_state": "idle", + "smart_weather": "sunny", + "use_time_one": 2 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sgbj_im2eqqhj72suwwko.json b/tests/components/tuya/fixtures/sgbj_im2eqqhj72suwwko.json new file mode 100644 index 00000000000000..1ceff88bb558bf --- /dev/null +++ b/tests/components/tuya/fixtures/sgbj_im2eqqhj72suwwko.json @@ -0,0 +1,92 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Siren", + "category": "sgbj", + "product_id": "im2eqqhj72suwwko", + "product_name": "Outdoor siren", + "online": true, + "sub": false, + "time_zone": "+10:00", + "active_time": "2025-08-24T05:42:34+00:00", + "create_time": "2025-08-24T05:42:34+00:00", + "update_time": "2025-08-24T05:42:34+00:00", + "function": { + "alarm_state": { + "type": "Enum", + "value": { + "range": ["alarm_sound", "alarm_light", "alarm_sound_light", "normal"] + } + }, + "alarm_volume": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high", "mute"] + } + }, + "alarm_time": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 10, + "scale": 0, + "step": 1 + } + } + }, + "status_range": { + "alarm_state": { + "type": "Enum", + "value": { + "range": ["alarm_sound", "alarm_light", "alarm_sound_light", "normal"] + } + }, + "alarm_volume": { + "type": "Enum", + "value": { + "range": ["low", "middle", "high", "mute"] + } + }, + "charge_state": { + "type": "Boolean", + "value": {} + }, + "alarm_time": { + "type": "Integer", + "value": { + "unit": "min", + "min": 1, + "max": 10, + "scale": 0, + "step": 1 + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + }, + "temper_alarm": { + "type": "Boolean", + "value": {} + } + }, + "status": { + "alarm_state": "normal", + "alarm_volume": "low", + "charge_state": true, + "alarm_time": 1, + "battery_percentage": 77, + "temper_alarm": true + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/sj_rzeSU2h9uoklxEwq.json b/tests/components/tuya/fixtures/sj_rzeSU2h9uoklxEwq.json new file mode 100644 index 00000000000000..1db78307f0dc2d --- /dev/null +++ b/tests/components/tuya/fixtures/sj_rzeSU2h9uoklxEwq.json @@ -0,0 +1,41 @@ +{ + "endpoint": "https://apigw.tuyaeu.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Inondation", + "category": "sj", + "product_id": "rzeSU2h9uoklxEwq", + "product_name": "WATER SENSOR", + "online": false, + "sub": true, + "time_zone": "+02:00", + "active_time": "2025-08-13T16:05:44+00:00", + "create_time": "2025-08-13T16:05:44+00:00", + "update_time": "2025-08-13T16:05:44+00:00", + "function": {}, + "status_range": { + "watersensor_state": { + "type": "Enum", + "value": { + "range": ["1", "2"] + } + }, + "battery_percentage": { + "type": "Integer", + "value": { + "unit": "%", + "min": 0, + "max": 100, + "scale": 0, + "step": 1 + } + } + }, + "status": { + "watersensor_state": 2, + "battery_percentage": 100 + }, + "set_up": true, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wg2_2gowdgni.json b/tests/components/tuya/fixtures/wg2_2gowdgni.json new file mode 100644 index 00000000000000..29b5204f72eafd --- /dev/null +++ b/tests/components/tuya/fixtures/wg2_2gowdgni.json @@ -0,0 +1,77 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Mesh-Gateway", + "category": "wg2", + "product_id": "2gowdgni", + "product_name": "Mesh-Gateway", + "online": true, + "sub": true, + "time_zone": "-05:00", + "active_time": "2024-07-29T18:45:22+00:00", + "create_time": "2024-07-29T18:45:22+00:00", + "update_time": "2024-07-29T18:45:22+00:00", + "function": { + "switch_alarm_sound": { + "type": "Boolean", + "value": {} + }, + "muffling": { + "type": "Boolean", + "value": {} + }, + "master_state": { + "type": "Enum", + "value": { + "range": ["normal", "alarm"] + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "alarm_active": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status_range": { + "switch_alarm_sound": { + "type": "Boolean", + "value": {} + }, + "muffling": { + "type": "Boolean", + "value": {} + }, + "master_state": { + "type": "Enum", + "value": { + "range": ["normal", "alarm"] + } + }, + "factory_reset": { + "type": "Boolean", + "value": {} + }, + "alarm_active": { + "type": "String", + "value": { + "maxlen": 255 + } + } + }, + "status": { + "switch_alarm_sound": false, + "muffling": false, + "master_state": "normal", + "factory_reset": false, + "alarm_active": "" + }, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wg2_tmwhss6ntjfc7prs.json b/tests/components/tuya/fixtures/wg2_tmwhss6ntjfc7prs.json new file mode 100644 index 00000000000000..ee188017887348 --- /dev/null +++ b/tests/components/tuya/fixtures/wg2_tmwhss6ntjfc7prs.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Gateway", + "category": "wg2", + "product_id": "tmwhss6ntjfc7prs", + "product_name": "Gateway", + "online": false, + "sub": true, + "time_zone": "-04:00", + "active_time": "2025-08-08T19:16:25+00:00", + "create_time": "2025-08-08T19:16:25+00:00", + "update_time": "2025-08-08T19:16:25+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/fixtures/wnykq_om518smspsaltzdi.json b/tests/components/tuya/fixtures/wnykq_om518smspsaltzdi.json new file mode 100644 index 00000000000000..537e96049534f4 --- /dev/null +++ b/tests/components/tuya/fixtures/wnykq_om518smspsaltzdi.json @@ -0,0 +1,21 @@ +{ + "endpoint": "https://apigw.tuyaus.com", + "mqtt_connected": true, + "disabled_by": null, + "disabled_polling": false, + "name": "Smart IR Theater", + "category": "wnykq", + "product_id": "om518smspsaltzdi", + "product_name": "Smart IR", + "online": false, + "sub": false, + "time_zone": "-05:00", + "active_time": "2025-08-08T21:06:47+00:00", + "create_time": "2025-08-08T21:06:47+00:00", + "update_time": "2025-08-08T21:06:47+00:00", + "function": {}, + "status_range": {}, + "status": {}, + "set_up": false, + "support_local": true +} diff --git a/tests/components/tuya/snapshots/test_binary_sensor.ambr b/tests/components/tuya/snapshots/test_binary_sensor.ambr index c2f246fb9e9d95..ad1838b675560a 100644 --- a/tests/components/tuya/snapshots/test_binary_sensor.ambr +++ b/tests/components/tuya/snapshots/test_binary_sensor.ambr @@ -734,6 +734,104 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[binary_sensor.inondation_moisture-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.inondation_moisture', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Moisture', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.qwExlkou9h2USezrjswatersensor_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.inondation_moisture-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'moisture', + 'friendly_name': 'Inondation Moisture', + }), + 'context': , + 'entity_id': 'binary_sensor.inondation_moisture', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.mesh_gateway_problem-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.mesh_gateway_problem', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Problem', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.ingdwog22gwmaster_state', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[binary_sensor.mesh_gateway_problem-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Mesh-Gateway Problem', + }), + 'context': , + 'entity_id': 'binary_sensor.mesh_gateway_problem', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[binary_sensor.motion_sensor_lidl_zigbee_motion-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_cover.ambr b/tests/components/tuya/snapshots/test_cover.ambr index 0ba09112408791..f18c96596b18f2 100644 --- a/tests/components/tuya/snapshots/test_cover.ambr +++ b/tests/components/tuya/snapshots/test_cover.ambr @@ -101,6 +101,56 @@ 'state': 'open', }) # --- +# name: test_platform_setup_and_discovery[cover.garage_door_door_1-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.garage_door_door_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'indexed_door', + 'unique_id': 'tuya.q3iie9vjd4wfqyy1qzkmkcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cover.garage_door_door_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'garage', + 'friendly_name': 'Garage door Door 1', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.garage_door_door_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- # name: test_platform_setup_and_discovery[cover.kitchen_blinds_blind-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -356,3 +406,54 @@ 'state': 'closed', }) # --- +# name: test_platform_setup_and_discovery[cover.vividstorm_screen_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.vividstorm_screen_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.4hbnivc4w2rsw966lccontrol', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[cover.vividstorm_screen_curtain-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'current_position': 100, + 'device_class': 'curtain', + 'friendly_name': 'VIVIDSTORM SCREEN Curtain', + 'supported_features': , + }), + 'context': , + 'entity_id': 'cover.vividstorm_screen_curtain', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'open', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_init.ambr b/tests/components/tuya/snapshots/test_init.ambr index 62e849a5509b7d..a70d38c6fbca91 100644 --- a/tests/components/tuya/snapshots/test_init.ambr +++ b/tests/components/tuya/snapshots/test_init.ambr @@ -309,6 +309,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[4hbnivc4w2rsw966lc] + 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', + '4hbnivc4w2rsw966lc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'VIVIDSTORM SCREEN', + 'model_id': '669wsr2w4cvinbh4', + 'name': 'VIVIDSTORM SCREEN', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[4pa1uobdjd] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -1270,6 +1301,68 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[VA4QyBNZHkJ2Xa4hjd] + 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', + 'VA4QyBNZHkJ2Xa4hjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Dimmer Switch', + 'model_id': 'h4aX2JkHZNByQ4AV', + 'name': 'Entry Stairs', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[YQLkAe7nyyAxXHiAzc] + 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', + 'YQLkAe7nyyAxXHiAzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Mini Smart Plug', + 'model_id': 'AiHXxAyyn7eAkLQY', + 'name': 'Solar Heater Pump', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[ZgXzZULP6dDp4Atvgcdsw] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -1735,6 +1828,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[bl5cuqxnqzkfs] + 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', + 'bl5cuqxnqzkfs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart Water Timer', + 'model_id': 'nxquc5lb', + 'name': 'Smart Water Timer', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[buzituffc13pgb1jjd] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -2107,6 +2231,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[dvdtmcoil5yopaljjzm] + 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', + 'dvdtmcoil5yopaljjzm', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'ISV-100W2.0 (unsupported)', + 'model_id': 'jlapoy5liocmtdvd', + 'name': 'ISV-100W2.0', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[e2sbdwuga5jorvejtkdy] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -2510,6 +2665,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[ggimpv4dqzkfs] + 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', + 'ggimpv4dqzkfs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Valve Controller', + 'model_id': 'd4vpmigg', + 'name': 'Garden Valve Yard', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[ggwxkj8bwn5y63flgcdsw] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -2603,6 +2789,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[glsehgu8jd] + 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', + 'glsehgu8jd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'POWERASIA', + 'model_id': '8ugheslg', + 'name': 'POWERASIA R2', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[gluaktf5gk] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -2913,6 +3130,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[idztlaspsms815moqkynw] + 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', + 'idztlaspsms815moqkynw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Smart IR (unsupported)', + 'model_id': 'om518smspsaltzdi', + 'name': 'Smart IR Theater', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[ifzgvpgoodrfw2aksc] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -3099,6 +3347,68 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[im3fum2zt73boagkjd] + 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', + 'im3fum2zt73boagkjd', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': '', + 'model_id': 'kgaob37tz2muf3mi', + 'name': 'Parker Ceiling Fan 1', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- +# name: test_device_registry[ingdwog22gw] + 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', + 'ingdwog22gw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Mesh-Gateway', + 'model_id': '2gowdgni', + 'name': 'Mesh-Gateway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[iomszlsve0yyzkfwqswwc] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -4091,6 +4401,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[okwwus27jhqqe2mijbgs] + 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', + 'okwwus27jhqqe2mijbgs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Outdoor siren', + 'model_id': 'im2eqqhj72suwwko', + 'name': 'Siren', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[ol8xwtcj42eg18bdbrnz] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -4463,6 +4804,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[q3iie9vjd4wfqyy1qzkmkc] + 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', + 'q3iie9vjd4wfqyy1qzkmkc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Garage door ', + 'model_id': '1yyqfw4djv9eii3q', + 'name': 'Garage door ', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[q62sg0p3s52thp6zzc] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -4649,6 +5021,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[qwExlkou9h2USezrjs] + 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', + 'qwExlkou9h2USezrjs', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'WATER SENSOR', + 'model_id': 'rzeSU2h9uoklxEwq', + 'name': 'Inondation', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[qyy1auihjyoogvb7zdccq] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -5083,6 +5486,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[srp7cfjtn6sshwmt2gw] + 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', + 'srp7cfjtn6sshwmt2gw', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Gateway (unsupported)', + 'model_id': 'tmwhss6ntjfc7prs', + 'name': 'Gateway', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[svjjuwykgijjedurps] DeviceRegistryEntrySnapshot({ 'area_id': None, @@ -6261,6 +6695,37 @@ 'via_device_id': None, }) # --- +# name: test_device_registry[zspc4q1ut7swycnyzc] + 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', + 'zspc4q1ut7swycnyzc', + ), + }), + 'labels': set({ + }), + 'manufacturer': 'Tuya', + 'model': 'Wi-Fi hub', + 'model_id': 'yncyws7tu1q4cpsz', + 'name': 'Wi-Fi hub', + 'name_by_user': None, + 'primary_config_entry': , + 'serial_number': None, + 'sw_version': None, + 'via_device_id': None, + }) +# --- # name: test_device_registry[zspxfhsvgn2hgtndzc] DeviceRegistryEntrySnapshot({ 'area_id': None, diff --git a/tests/components/tuya/snapshots/test_light.ambr b/tests/components/tuya/snapshots/test_light.ambr index c18de2a2285882..c04cee4a46dff1 100644 --- a/tests/components/tuya/snapshots/test_light.ambr +++ b/tests/components/tuya/snapshots/test_light.ambr @@ -992,6 +992,62 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[light.entry_stairs-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + '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.entry_stairs', + '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.VA4QyBNZHkJ2Xa4hjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.entry_stairs-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Entry Stairs', + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.entry_stairs', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[light.erker_1_gold-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2225,6 +2281,72 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[light.parker_ceiling_fan_1-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.parker_ceiling_fan_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': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'tuya.im3fum2zt73boagkjdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.parker_ceiling_fan_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Parker Ceiling Fan 1', + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'supported_color_modes': list([ + , + , + ]), + 'supported_features': , + }), + 'context': , + 'entity_id': 'light.parker_ceiling_fan_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[light.pokerlamp_1-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2416,6 +2538,77 @@ 'state': 'off', }) # --- +# name: test_platform_setup_and_discovery[light.powerasia_r2-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.powerasia_r2', + '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.glsehgu8jdswitch_led', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[light.powerasia_r2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'brightness': None, + 'color_mode': , + 'color_temp': None, + 'color_temp_kelvin': None, + 'friendly_name': 'POWERASIA R2', + 'hs_color': None, + 'max_color_temp_kelvin': 6500, + 'max_mireds': 500, + 'min_color_temp_kelvin': 2000, + 'min_mireds': 153, + 'rgb_color': None, + 'supported_color_modes': list([ + , + ]), + 'supported_features': , + 'xy_color': None, + }), + 'context': , + 'entity_id': 'light.powerasia_r2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[light.sjiethoes-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_number.ambr b/tests/components/tuya/snapshots/test_number.ambr index 1aa8c3dcca90be..bc49b03cd364a1 100644 --- a/tests/components/tuya/snapshots/test_number.ambr +++ b/tests/components/tuya/snapshots/test_number.ambr @@ -1642,6 +1642,64 @@ 'state': '0.1', }) # --- +# name: test_platform_setup_and_discovery[number.siren_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.siren_time', + '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': 'Time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'time', + 'unique_id': 'tuya.okwwus27jhqqe2mijbgsalarm_time', + 'unit_of_measurement': 'min', + }) +# --- +# name: test_platform_setup_and_discovery[number.siren_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren Time', + 'max': 10.0, + 'min': 1.0, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': 'min', + }), + 'context': , + 'entity_id': 'number.siren_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1.0', + }) +# --- # name: test_platform_setup_and_discovery[number.siren_veranda_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_select.ambr b/tests/components/tuya/snapshots/test_select.ambr index fc238604ea3e41..7c68a647040097 100644 --- a/tests/components/tuya/snapshots/test_select.ambr +++ b/tests/components/tuya/snapshots/test_select.ambr @@ -2416,6 +2416,67 @@ 'state': 'unknown', }) # --- +# name: test_platform_setup_and_discovery[select.garden_valve_yard_weather_delay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cancel', + '24h', + '48h', + '72h', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.garden_valve_yard_weather_delay', + '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': 'Weather delay', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'weather_delay', + 'unique_id': 'tuya.ggimpv4dqzkfsweather_delay', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.garden_valve_yard_weather_delay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Valve Yard Weather delay', + 'options': list([ + 'cancel', + '24h', + '48h', + '72h', + ]), + }), + 'context': , + 'entity_id': 'select.garden_valve_yard_weather_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'cancel', + }) +# --- # name: test_platform_setup_and_discovery[select.hoover_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3372,6 +3433,67 @@ 'state': 'middle', }) # --- +# name: test_platform_setup_and_discovery[select.siren_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'low', + 'middle', + 'high', + 'mute', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.siren_volume', + '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': 'Volume', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'volume', + 'unique_id': 'tuya.okwwus27jhqqe2mijbgsalarm_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.siren_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Siren Volume', + 'options': list([ + 'low', + 'middle', + 'high', + 'mute', + ]), + }), + 'context': , + 'entity_id': 'select.siren_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'low', + }) +# --- # name: test_platform_setup_and_discovery[select.smart_odor_eliminator_pro_odor_elimination_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3429,6 +3551,67 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[select.smart_water_timer_weather_delay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'cancel', + '24h', + '48h', + '72h', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.smart_water_timer_weather_delay', + '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': 'Weather delay', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'weather_delay', + 'unique_id': 'tuya.bl5cuqxnqzkfsweather_delay', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.smart_water_timer_weather_delay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart Water Timer Weather delay', + 'options': list([ + 'cancel', + '24h', + '48h', + '72h', + ]), + }), + 'context': , + 'entity_id': 'select.smart_water_timer_weather_delay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[select.socket3_power_on_behavior-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4127,6 +4310,63 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[select.vividstorm_screen_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.vividstorm_screen_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.4hbnivc4w2rsw966lccontrol_back_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.vividstorm_screen_motor_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'VIVIDSTORM SCREEN Motor mode', + 'options': list([ + 'forward', + 'back', + ]), + }), + 'context': , + 'entity_id': 'select.vividstorm_screen_motor_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'forward', + }) +# --- # name: test_platform_setup_and_discovery[select.wallwasher_front_indicator_light_mode-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4363,3 +4603,62 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[select.wi_fi_hub_power_on_behavior-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': , + 'entity_id': 'select.wi_fi_hub_power_on_behavior', + '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': 'Power on behavior', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay_status', + 'unique_id': 'tuya.zspc4q1ut7swycnyzcrelay_status', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[select.wi_fi_hub_power_on_behavior-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wi-Fi hub Power on behavior', + 'options': list([ + 'power_off', + 'power_on', + 'last', + ]), + }), + 'context': , + 'entity_id': 'select.wi_fi_hub_power_on_behavior', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'power_on', + }) +# --- diff --git a/tests/components/tuya/snapshots/test_sensor.ambr b/tests/components/tuya/snapshots/test_sensor.ambr index 0baf85f05b636c..6c11d6034b8f67 100644 --- a/tests/components/tuya/snapshots/test_sensor.ambr +++ b/tests/components/tuya/snapshots/test_sensor.ambr @@ -5354,6 +5354,111 @@ 'state': '240.7', }) # --- +# name: test_platform_setup_and_discovery[sensor.garden_valve_yard_battery-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.garden_valve_yard_battery', + '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', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.ggimpv4dqzkfsbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garden_valve_yard_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Garden Valve Yard Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.garden_valve_yard_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100.0', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garden_valve_yard_total_watering_time-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.garden_valve_yard_total_watering_time', + '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': 'Total watering time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_watering_time', + 'unique_id': 'tuya.ggimpv4dqzkfstime_use', + 'unit_of_measurement': 's', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.garden_valve_yard_total_watering_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Valve Yard Total watering time', + 'state_class': , + 'unit_of_measurement': 's', + }), + 'context': , + 'entity_id': 'sensor.garden_valve_yard_total_watering_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '38201.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.gas_sensor_gas-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6655,6 +6760,59 @@ 'state': '232.1', }) # --- +# name: test_platform_setup_and_discovery[sensor.inondation_battery-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.inondation_battery', + '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', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.qwExlkou9h2USezrjsbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.inondation_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Inondation Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.inondation_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[sensor.ion1000pro_pm2_5-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -10863,6 +11021,111 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[sensor.smart_water_timer_battery-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.smart_water_timer_battery', + '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', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery', + 'unique_id': 'tuya.bl5cuqxnqzkfsbattery_percentage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_water_timer_battery-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Smart Water Timer Battery', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.smart_water_timer_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_water_timer_total_watering_time-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.smart_water_timer_total_watering_time', + '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': 'Total watering time', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'total_watering_time', + 'unique_id': 'tuya.bl5cuqxnqzkfstime_use', + 'unit_of_measurement': 's', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.smart_water_timer_total_watering_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart Water Timer Total watering time', + 'state_class': , + 'unit_of_measurement': 's', + }), + 'context': , + 'entity_id': 'sensor.smart_water_timer_total_watering_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[sensor.smogo_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -12845,6 +13108,55 @@ 'state': '224.6', }) # --- +# name: test_platform_setup_and_discovery[sensor.vividstorm_screen_last_operation_duration-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': , + 'entity_id': 'sensor.vividstorm_screen_last_operation_duration', + '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': 'Last operation duration', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'last_operation_duration', + 'unique_id': 'tuya.4hbnivc4w2rsw966lctime_total', + 'unit_of_measurement': 'ms', + }) +# --- +# name: test_platform_setup_and_discovery[sensor.vividstorm_screen_last_operation_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'VIVIDSTORM SCREEN Last operation duration', + 'unit_of_measurement': 'ms', + }), + 'context': , + 'entity_id': 'sensor.vividstorm_screen_last_operation_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- # name: test_platform_setup_and_discovery[sensor.water_fountain_filter_duration-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/tuya/snapshots/test_switch.ambr b/tests/components/tuya/snapshots/test_switch.ambr index 147c18e9e2a885..97ba2e47e110bb 100644 --- a/tests/components/tuya/snapshots/test_switch.ambr +++ b/tests/components/tuya/snapshots/test_switch.ambr @@ -3871,6 +3871,54 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[switch.garden_valve_yard_switch-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': None, + 'entity_id': 'switch.garden_valve_yard_switch', + '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': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.ggimpv4dqzkfsswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.garden_valve_yard_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Garden Valve Yard Switch', + }), + 'context': , + 'entity_id': 'switch.garden_valve_yard_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[switch.hl400_child_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5224,6 +5272,54 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[switch.mesh_gateway_mute-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.mesh_gateway_mute', + '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': 'Mute', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mute', + 'unique_id': 'tuya.ingdwog22gwmuffling', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.mesh_gateway_mute-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Mesh-Gateway Mute', + }), + 'context': , + 'entity_id': 'switch.mesh_gateway_mute', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_platform_setup_and_discovery[switch.multifunction_alarm_arm_beep-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -6826,6 +6922,54 @@ 'state': 'on', }) # --- +# name: test_platform_setup_and_discovery[switch.smart_water_timer_switch-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': None, + 'entity_id': 'switch.smart_water_timer_switch', + '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': 'Switch', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'switch', + 'unique_id': 'tuya.bl5cuqxnqzkfsswitch', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.smart_water_timer_switch-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Smart Water Timer Switch', + }), + 'context': , + 'entity_id': 'switch.smart_water_timer_switch', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unavailable', + }) +# --- # name: test_platform_setup_and_discovery[switch.smoke_alarm_mute-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -7068,6 +7212,55 @@ 'state': 'unavailable', }) # --- +# name: test_platform_setup_and_discovery[switch.solar_heater_pump_socket_1-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': None, + 'entity_id': 'switch.solar_heater_pump_socket_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Socket 1', + 'platform': 'tuya', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'indexed_socket', + 'unique_id': 'tuya.YQLkAe7nyyAxXHiAzcswitch_1', + 'unit_of_measurement': None, + }) +# --- +# name: test_platform_setup_and_discovery[switch.solar_heater_pump_socket_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'outlet', + 'friendly_name': 'Solar Heater Pump Socket 1', + }), + 'context': , + 'entity_id': 'switch.solar_heater_pump_socket_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_platform_setup_and_discovery[switch.solar_zijpad_energy_saving-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 58d4fd0b755adf8f965dde680b741d7b0674f8c8 Mon Sep 17 00:00:00 2001 From: Petro31 <35082313+Petro31@users.noreply.github.com> Date: Mon, 25 Aug 2025 08:43:23 -0400 Subject: [PATCH 2/5] Add update platform to template integration (#150277) --- homeassistant/components/template/config.py | 5 + .../components/template/config_flow.py | 51 + homeassistant/components/template/const.py | 1 + .../components/template/strings.json | 90 ++ homeassistant/components/template/update.py | 463 +++++++ .../template/snapshots/test_update.ambr | 26 + tests/components/template/test_config_flow.py | 32 + tests/components/template/test_init.py | 12 + tests/components/template/test_update.py | 1085 +++++++++++++++++ 9 files changed, 1765 insertions(+) create mode 100644 homeassistant/components/template/update.py create mode 100644 tests/components/template/snapshots/test_update.ambr create mode 100644 tests/components/template/test_update.py diff --git a/homeassistant/components/template/config.py b/homeassistant/components/template/config.py index 092dbc9e41e800..ad2402bb98084e 100644 --- a/homeassistant/components/template/config.py +++ b/homeassistant/components/template/config.py @@ -26,6 +26,7 @@ from homeassistant.components.select import DOMAIN as DOMAIN_SELECT from homeassistant.components.sensor import DOMAIN as DOMAIN_SENSOR from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH +from homeassistant.components.update import DOMAIN as DOMAIN_UPDATE from homeassistant.components.vacuum import DOMAIN as DOMAIN_VACUUM from homeassistant.components.weather import DOMAIN as DOMAIN_WEATHER from homeassistant.config import async_log_schema_error, config_without_domain @@ -63,6 +64,7 @@ select as select_platform, sensor as sensor_platform, switch as switch_platform, + update as update_platform, vacuum as vacuum_platform, weather as weather_platform, ) @@ -153,6 +155,9 @@ def _backward_compat_schema(value: Any | None) -> Any: vol.Optional(DOMAIN_SWITCH): vol.All( cv.ensure_list, [switch_platform.SWITCH_YAML_SCHEMA] ), + vol.Optional(DOMAIN_UPDATE): vol.All( + cv.ensure_list, [update_platform.UPDATE_YAML_SCHEMA] + ), vol.Optional(DOMAIN_VACUUM): vol.All( cv.ensure_list, [vacuum_platform.VACUUM_YAML_SCHEMA] ), diff --git a/homeassistant/components/template/config_flow.py b/homeassistant/components/template/config_flow.py index 745e2933c58b50..36c27aa19f95c8 100644 --- a/homeassistant/components/template/config_flow.py +++ b/homeassistant/components/template/config_flow.py @@ -20,6 +20,7 @@ SensorDeviceClass, SensorStateClass, ) +from homeassistant.components.update import UpdateDeviceClass from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_DEVICE_ID, @@ -106,6 +107,19 @@ from .sensor import async_create_preview_sensor from .switch import async_create_preview_switch from .template_entity import TemplateEntity +from .update import ( + CONF_BACKUP, + CONF_IN_PROGRESS, + CONF_INSTALL, + CONF_INSTALLED_VERSION, + CONF_LATEST_VERSION, + CONF_RELEASE_SUMMARY, + CONF_RELEASE_URL, + CONF_SPECIFIC_VERSION, + CONF_TITLE, + CONF_UPDATE_PERCENTAGE, + async_create_preview_update, +) from .vacuum import ( CONF_FAN_SPEED, CONF_FAN_SPEED_LIST, @@ -335,6 +349,31 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema: vol.Optional(CONF_TURN_OFF): selector.ActionSelector(), } + if domain == Platform.UPDATE: + schema |= { + vol.Optional(CONF_INSTALLED_VERSION): selector.TemplateSelector(), + vol.Optional(CONF_LATEST_VERSION): selector.TemplateSelector(), + vol.Optional(CONF_INSTALL): selector.ActionSelector(), + vol.Optional(CONF_IN_PROGRESS): selector.TemplateSelector(), + vol.Optional(CONF_RELEASE_SUMMARY): selector.TemplateSelector(), + vol.Optional(CONF_RELEASE_URL): selector.TemplateSelector(), + vol.Optional(CONF_TITLE): selector.TemplateSelector(), + vol.Optional(CONF_UPDATE_PERCENTAGE): selector.TemplateSelector(), + vol.Optional(CONF_BACKUP): selector.BooleanSelector(), + vol.Optional(CONF_SPECIFIC_VERSION): selector.BooleanSelector(), + } + if flow_type == "config": + schema |= { + vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector( + selector.SelectSelectorConfig( + options=[cls.value for cls in UpdateDeviceClass], + mode=selector.SelectSelectorMode.DROPDOWN, + translation_key="update_device_class", + sort=True, + ), + ), + } + if domain == Platform.VACUUM: schema |= _SCHEMA_STATE | { vol.Required(SERVICE_START): selector.ActionSelector(), @@ -470,6 +509,7 @@ async def _validate_user_input( Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, Platform.VACUUM, ] @@ -539,6 +579,11 @@ async def _validate_user_input( preview="template", validate_user_input=validate_user_input(Platform.SWITCH), ), + Platform.UPDATE: SchemaFlowFormStep( + config_schema(Platform.UPDATE), + preview="template", + validate_user_input=validate_user_input(Platform.UPDATE), + ), Platform.VACUUM: SchemaFlowFormStep( config_schema(Platform.VACUUM), preview="template", @@ -613,6 +658,11 @@ async def _validate_user_input( preview="template", validate_user_input=validate_user_input(Platform.SWITCH), ), + Platform.UPDATE: SchemaFlowFormStep( + options_schema(Platform.UPDATE), + preview="template", + validate_user_input=validate_user_input(Platform.UPDATE), + ), Platform.VACUUM: SchemaFlowFormStep( options_schema(Platform.VACUUM), preview="template", @@ -635,6 +685,7 @@ async def _validate_user_input( Platform.SELECT: async_create_preview_select, Platform.SENSOR: async_create_preview_sensor, Platform.SWITCH: async_create_preview_switch, + Platform.UPDATE: async_create_preview_update, Platform.VACUUM: async_create_preview_vacuum, } diff --git a/homeassistant/components/template/const.py b/homeassistant/components/template/const.py index 43b5fcc255affd..5ff2c0137ac41a 100644 --- a/homeassistant/components/template/const.py +++ b/homeassistant/components/template/const.py @@ -47,6 +47,7 @@ Platform.SELECT, Platform.SENSOR, Platform.SWITCH, + Platform.UPDATE, Platform.VACUUM, Platform.WEATHER, ] diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index 6de26d885cbb49..c565023f7de3ad 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -393,6 +393,7 @@ "select": "Template a select", "sensor": "Template a sensor", "switch": "Template a switch", + "update": "Template an update", "vacuum": "Template a vacuum" }, "title": "Template helper" @@ -424,6 +425,48 @@ }, "title": "Template switch" }, + "update": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "device_class": "[%key:component::template::common::device_class%]", + "name": "[%key:common::config_flow::data::name%]", + "installed_version": "[%key:component::update::entity_component::_::state_attributes::installed_version::name%]", + "latest_version": "[%key:component::update::entity_component::_::state_attributes::latest_version::name%]", + "install": "Actions on install", + "in_progress": "[%key:component::update::entity_component::_::state_attributes::in_progress::name%]", + "release_summary": "[%key:component::update::entity_component::_::state_attributes::release_summary::name%]", + "release_url": "[%key:component::update::entity_component::_::state_attributes::release_url::name%]", + "title": "[%key:component::update::entity_component::_::state_attributes::title::name%]", + "backup": "Backup", + "specific_version": "Specific version", + "update_percent": "Update percentage" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "installed_version": "Defines a template to get the installed version.", + "latest_version": "Defines a template to get the latest version.", + "install": "Defines actions to run when the update is installed. Receives variables `specific_version` and `backup` when enabled.", + "in_progress": "Defines a template to get the in-progress state.", + "release_summary": "Defines a template to get the release summary.", + "release_url": "Defines a template to get the release URL.", + "title": "Defines a template to get the update title.", + "backup": "Enable or disable the `automatic backup before update` option in the update repair. When disabled, the `backup` variable will always provide `False` during the `install` action and it will not accept the `backup` option.", + "specific_version": "Enable or disable using the `version` variable with the `install` action. When disabled, the `specific_version` variable will always provide `None` in the `install` actions", + "update_percent": "Defines a template to get the update completion percentage." + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "Template update" + }, "vacuum": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -853,6 +896,48 @@ }, "title": "[%key:component::template::config::step::switch::title%]" }, + "update": { + "data": { + "device_id": "[%key:common::config_flow::data::device%]", + "device_class": "[%key:component::template::common::device_class%]", + "name": "[%key:common::config_flow::data::name%]", + "installed_version": "[%key:component::update::entity_component::_::state_attributes::installed_version::name%]", + "latest_version": "[%key:component::update::entity_component::_::state_attributes::latest_version::name%]", + "install": "[%key:component::template::config::step::update::data::install%]", + "in_progress": "[%key:component::update::entity_component::_::state_attributes::in_progress::name%]", + "release_summary": "[%key:component::update::entity_component::_::state_attributes::release_summary::name%]", + "release_url": "[%key:component::update::entity_component::_::state_attributes::release_url::name%]", + "title": "[%key:component::update::entity_component::_::state_attributes::title::name%]", + "backup": "[%key:component::template::config::step::update::data::backup%]", + "specific_version": "[%key:component::template::config::step::update::data::specific_version%]", + "update_percent": "[%key:component::template::config::step::update::data::update_percent%]" + }, + "data_description": { + "device_id": "[%key:component::template::common::device_id_description%]", + "installed_version": "[%key:component::template::config::step::update::data_description::installed_version%]", + "latest_version": "[%key:component::template::config::step::update::data_description::latest_version%]", + "install": "[%key:component::template::config::step::update::data_description::install%]", + "in_progress": "[%key:component::template::config::step::update::data_description::in_progress%]", + "release_summary": "[%key:component::template::config::step::update::data_description::release_summary%]", + "release_url": "[%key:component::template::config::step::update::data_description::release_url%]", + "title": "[%key:component::template::config::step::update::data_description::title%]", + "backup": "[%key:component::template::config::step::update::data_description::backup%]", + "specific_version": "[%key:component::template::config::step::update::data_description::specific_version%]", + "update_percent": "[%key:component::template::config::step::update::data_description::update_percent%]" + }, + "sections": { + "advanced_options": { + "name": "[%key:component::template::common::advanced_options%]", + "data": { + "availability": "[%key:component::template::common::availability%]" + }, + "data_description": { + "availability": "[%key:component::template::common::availability_description%]" + } + } + }, + "title": "Template update" + }, "vacuum": { "data": { "device_id": "[%key:common::config_flow::data::device%]", @@ -1037,6 +1122,11 @@ "options": { "none": "No unit of measurement" } + }, + "update_device_class": { + "options": { + "firmware": "[%key:component::update::entity_component::firmware::name%]" + } } }, "services": { diff --git a/homeassistant/components/template/update.py b/homeassistant/components/template/update.py new file mode 100644 index 00000000000000..a6b0bca0f5f78f --- /dev/null +++ b/homeassistant/components/template/update.py @@ -0,0 +1,463 @@ +"""Support for updates which integrates with other components.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +import voluptuous as vol + +from homeassistant.components.update import ( + ATTR_INSTALLED_VERSION, + ATTR_LATEST_VERSION, + DEVICE_CLASSES_SCHEMA, + DOMAIN as UPDATE_DOMAIN, + ENTITY_ID_FORMAT, + UpdateEntity, + UpdateEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_DEVICE_CLASS, + CONF_NAME, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.entity_platform import ( + AddConfigEntryEntitiesCallback, + AddEntitiesCallback, +) +from homeassistant.helpers.template import _SENTINEL +from homeassistant.helpers.trigger_template_entity import CONF_PICTURE +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from . import TriggerUpdateCoordinator +from .const import DOMAIN +from .entity import AbstractTemplateEntity +from .helpers import ( + async_setup_template_entry, + async_setup_template_platform, + async_setup_template_preview, +) +from .template_entity import ( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA, + TemplateEntity, + make_template_entity_common_modern_schema, +) +from .trigger_entity import TriggerEntity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Template Update" + +ATTR_BACKUP = "backup" +ATTR_SPECIFIC_VERSION = "specific_version" + +CONF_BACKUP = "backup" +CONF_IN_PROGRESS = "in_progress" +CONF_INSTALL = "install" +CONF_INSTALLED_VERSION = "installed_version" +CONF_LATEST_VERSION = "latest_version" +CONF_RELEASE_SUMMARY = "release_summary" +CONF_RELEASE_URL = "release_url" +CONF_SPECIFIC_VERSION = "specific_version" +CONF_TITLE = "title" +CONF_UPDATE_PERCENTAGE = "update_percentage" + +UPDATE_COMMON_SCHEMA = vol.Schema( + { + vol.Optional(CONF_BACKUP, default=False): cv.boolean, + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_IN_PROGRESS): cv.template, + vol.Optional(CONF_INSTALL): cv.SCRIPT_SCHEMA, + vol.Required(CONF_INSTALLED_VERSION): cv.template, + vol.Required(CONF_LATEST_VERSION): cv.template, + vol.Optional(CONF_RELEASE_SUMMARY): cv.template, + vol.Optional(CONF_RELEASE_URL): cv.template, + vol.Optional(CONF_SPECIFIC_VERSION, default=False): cv.boolean, + vol.Optional(CONF_TITLE): cv.template, + vol.Optional(CONF_UPDATE_PERCENTAGE): cv.template, + } +) + +UPDATE_YAML_SCHEMA = UPDATE_COMMON_SCHEMA.extend( + make_template_entity_common_modern_schema(DEFAULT_NAME).schema +) + +UPDATE_CONFIG_ENTRY_SCHEMA = UPDATE_COMMON_SCHEMA.extend( + TEMPLATE_ENTITY_COMMON_CONFIG_ENTRY_SCHEMA.schema +) + + +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up the Template update.""" + await async_setup_template_platform( + hass, + UPDATE_DOMAIN, + config, + StateUpdateEntity, + TriggerUpdateEntity, + async_add_entities, + discovery_info, + ) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Initialize config entry.""" + await async_setup_template_entry( + hass, + config_entry, + async_add_entities, + StateUpdateEntity, + UPDATE_CONFIG_ENTRY_SCHEMA, + ) + + +@callback +def async_create_preview_update( + hass: HomeAssistant, name: str, config: dict[str, Any] +) -> StateUpdateEntity: + """Create a preview.""" + return async_setup_template_preview( + hass, + name, + config, + StateUpdateEntity, + UPDATE_CONFIG_ENTRY_SCHEMA, + ) + + +class AbstractTemplateUpdate(AbstractTemplateEntity, UpdateEntity): + """Representation of a template update features.""" + + _entity_id_format = ENTITY_ID_FORMAT + + # The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__. + # This ensures that the __init__ on AbstractTemplateEntity is not called twice. + def __init__(self, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called + """Initialize the features.""" + + self._installed_version_template = config[CONF_INSTALLED_VERSION] + self._latest_version_template = config[CONF_LATEST_VERSION] + + self._attr_device_class = config.get(CONF_DEVICE_CLASS) + + self._in_progress_template = config.get(CONF_IN_PROGRESS) + self._release_summary_template = config.get(CONF_RELEASE_SUMMARY) + self._release_url_template = config.get(CONF_RELEASE_URL) + self._title_template = config.get(CONF_TITLE) + self._update_percentage_template = config.get(CONF_UPDATE_PERCENTAGE) + + self._attr_supported_features = UpdateEntityFeature(0) + if config[CONF_BACKUP]: + self._attr_supported_features |= UpdateEntityFeature.BACKUP + if config[CONF_SPECIFIC_VERSION]: + self._attr_supported_features |= UpdateEntityFeature.SPECIFIC_VERSION + if ( + self._in_progress_template is not None + or self._update_percentage_template is not None + ): + self._attr_supported_features |= UpdateEntityFeature.PROGRESS + + self._optimistic_in_process = ( + self._in_progress_template is None + and self._update_percentage_template is not None + ) + + @callback + def _update_installed_version(self, result: Any) -> None: + if result is None: + self._attr_installed_version = None + return + + self._attr_installed_version = cv.string(result) + + @callback + def _update_latest_version(self, result: Any) -> None: + if result is None: + self._attr_latest_version = None + return + + self._attr_latest_version = cv.string(result) + + @callback + def _update_in_process(self, result: Any) -> None: + try: + self._attr_in_progress = cv.boolean(result) + except vol.Invalid: + _LOGGER.error( + "Received invalid in_process value: %s for entity %s. Expected: True, False", + result, + self.entity_id, + ) + self._attr_in_progress = False + + @callback + def _update_release_summary(self, result: Any) -> None: + if result is None: + self._attr_release_summary = None + return + + self._attr_release_summary = cv.string(result) + + @callback + def _update_release_url(self, result: Any) -> None: + if result is None: + self._attr_release_url = None + return + + try: + self._attr_release_url = cv.url(result) + except vol.Invalid: + _LOGGER.error( + "Received invalid release_url: %s for entity %s", + result, + self.entity_id, + ) + self._attr_release_url = None + + @callback + def _update_title(self, result: Any) -> None: + if result is None: + self._attr_title = None + return + + self._attr_title = cv.string(result) + + @callback + def _update_update_percentage(self, result: Any) -> None: + if result is None: + if self._optimistic_in_process: + self._attr_in_progress = False + self._attr_update_percentage = None + return + + try: + percentage = vol.All( + vol.Coerce(float), + vol.Range(0, 100, min_included=True, max_included=True), + )(result) + if self._optimistic_in_process: + self._attr_in_progress = True + self._attr_update_percentage = percentage + except vol.Invalid: + _LOGGER.error( + "Received invalid update_percentage: %s for entity %s", + result, + self.entity_id, + ) + self._attr_update_percentage = None + + async def async_install( + self, version: str | None, backup: bool, **kwargs: Any + ) -> None: + """Install an update.""" + await self.async_run_script( + self._action_scripts[CONF_INSTALL], + run_variables={ATTR_SPECIFIC_VERSION: version, ATTR_BACKUP: backup}, + context=self._context, + ) + + +class StateUpdateEntity(TemplateEntity, AbstractTemplateUpdate): + """Representation of a Template update.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + config: ConfigType, + unique_id: str | None, + ) -> None: + """Initialize the Template update.""" + TemplateEntity.__init__(self, hass, config, unique_id) + AbstractTemplateUpdate.__init__(self, config) + + name = self._attr_name + if TYPE_CHECKING: + assert name is not None + + # Scripts can be an empty list, therefore we need to check for None + if (install_action := config.get(CONF_INSTALL)) is not None: + self.add_script(CONF_INSTALL, install_action, name, DOMAIN) + self._attr_supported_features |= UpdateEntityFeature.INSTALL + + @property + def entity_picture(self) -> str | None: + """Return the entity picture to use in the frontend.""" + # This is needed to override the base update entity functionality + if self._attr_entity_picture is None: + # The default picture for update entities would use `self.platform.platform_name` in + # place of `template`. This does not work when creating an entity preview because + # the platform does not exist for that entity, therefore this is hardcoded as `template`. + return "https://brands.home-assistant.io/_/template/icon.png" + return self._attr_entity_picture + + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + self.add_template_attribute( + "_attr_installed_version", + self._installed_version_template, + None, + self._update_installed_version, + none_on_template_error=True, + ) + self.add_template_attribute( + "_attr_latest_version", + self._latest_version_template, + None, + self._update_latest_version, + none_on_template_error=True, + ) + if self._in_progress_template is not None: + self.add_template_attribute( + "_attr_in_progress", + self._in_progress_template, + None, + self._update_in_process, + none_on_template_error=True, + ) + if self._release_summary_template is not None: + self.add_template_attribute( + "_attr_release_summary", + self._release_summary_template, + None, + self._update_release_summary, + none_on_template_error=True, + ) + if self._release_url_template is not None: + self.add_template_attribute( + "_attr_release_url", + self._release_url_template, + None, + self._update_release_url, + none_on_template_error=True, + ) + if self._title_template is not None: + self.add_template_attribute( + "_attr_title", + self._title_template, + None, + self._update_title, + none_on_template_error=True, + ) + if self._update_percentage_template is not None: + self.add_template_attribute( + "_attr_update_percentage", + self._update_percentage_template, + None, + self._update_update_percentage, + none_on_template_error=True, + ) + super()._async_setup_templates() + + +class TriggerUpdateEntity(TriggerEntity, AbstractTemplateUpdate): + """Update entity based on trigger data.""" + + domain = UPDATE_DOMAIN + + def __init__( + self, + hass: HomeAssistant, + coordinator: TriggerUpdateCoordinator, + config: ConfigType, + ) -> None: + """Initialize the entity.""" + TriggerEntity.__init__(self, hass, coordinator, config) + AbstractTemplateUpdate.__init__(self, config) + + for key in ( + CONF_INSTALLED_VERSION, + CONF_LATEST_VERSION, + ): + self._to_render_simple.append(key) + self._parse_result.add(key) + + # Scripts can be an empty list, therefore we need to check for None + if (install_action := config.get(CONF_INSTALL)) is not None: + self.add_script( + CONF_INSTALL, + install_action, + self._rendered.get(CONF_NAME, DEFAULT_NAME), + DOMAIN, + ) + self._attr_supported_features |= UpdateEntityFeature.INSTALL + + for key in ( + CONF_IN_PROGRESS, + CONF_RELEASE_SUMMARY, + CONF_RELEASE_URL, + CONF_TITLE, + CONF_UPDATE_PERCENTAGE, + ): + if isinstance(config.get(key), template.Template): + self._to_render_simple.append(key) + self._parse_result.add(key) + + # Ensure the entity picture can resolve None to produce the default picture. + if CONF_PICTURE in config: + self._parse_result.add(CONF_PICTURE) + + async def async_added_to_hass(self) -> None: + """Restore last state.""" + await super().async_added_to_hass() + if ( + (last_state := await self.async_get_last_state()) is not None + and last_state.state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) + and self._attr_installed_version is None + and self._attr_latest_version is None + ): + self._attr_installed_version = last_state.attributes[ATTR_INSTALLED_VERSION] + self._attr_latest_version = last_state.attributes[ATTR_LATEST_VERSION] + self.restore_attributes(last_state) + + @property + def entity_picture(self) -> str | None: + """Return entity picture.""" + if (picture := self._rendered.get(CONF_PICTURE)) is None: + return UpdateEntity.entity_picture.fget(self) # type: ignore[attr-defined] + return picture + + @callback + def _handle_coordinator_update(self) -> None: + """Handle update of the data.""" + self._process_data() + + if not self.available: + self.async_write_ha_state() + return + + write_ha_state = False + for key, updater in ( + (CONF_INSTALLED_VERSION, self._update_installed_version), + (CONF_LATEST_VERSION, self._update_latest_version), + (CONF_IN_PROGRESS, self._update_in_process), + (CONF_RELEASE_SUMMARY, self._update_release_summary), + (CONF_RELEASE_URL, self._update_release_url), + (CONF_TITLE, self._update_title), + (CONF_UPDATE_PERCENTAGE, self._update_update_percentage), + ): + if (rendered := self._rendered.get(key, _SENTINEL)) is not _SENTINEL: + updater(rendered) + write_ha_state = True + + if len(self._rendered) > 0: + # In case any non optimistic template + write_ha_state = True + + if write_ha_state: + self.async_write_ha_state() diff --git a/tests/components/template/snapshots/test_update.ambr b/tests/components/template/snapshots/test_update.ambr new file mode 100644 index 00000000000000..479ccb88ffcaf4 --- /dev/null +++ b/tests/components/template/snapshots/test_update.ambr @@ -0,0 +1,26 @@ +# serializer version: 1 +# name: test_setup_config_entry + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'auto_update': False, + 'display_precision': 0, + 'entity_picture': 'https://brands.home-assistant.io/_/template/icon.png', + 'friendly_name': 'template_update', + 'in_progress': False, + 'installed_version': '1.0', + 'latest_version': '2.0', + 'release_summary': None, + 'release_url': None, + 'skipped_version': None, + 'supported_features': , + 'title': None, + 'update_percentage': None, + }), + 'context': , + 'entity_id': 'update.template_update', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/template/test_config_flow.py b/tests/components/template/test_config_flow.py index 49a9d5a1e5fbbf..3bf7b836a8bcf9 100644 --- a/tests/components/template/test_config_flow.py +++ b/tests/components/template/test_config_flow.py @@ -249,6 +249,16 @@ {}, {}, ), + ( + "update", + {"installed_version": "{{ states('update.one') }}"}, + "off", + {"one": "2.0", "two": "1.0"}, + {}, + {"latest_version": "{{ '2.0' }}"}, + {"latest_version": "{{ '2.0' }}"}, + {}, + ), ( "vacuum", {"state": "{{ states('vacuum.one') }}"}, @@ -440,6 +450,12 @@ async def test_config_flow( {"options": "{{ ['off', 'on', 'auto'] }}"}, {"options": "{{ ['off', 'on', 'auto'] }}"}, ), + ( + "update", + {"installed_version": "{{ states('update.one') }}"}, + {"latest_version": "{{ '2.0' }}"}, + {"latest_version": "{{ '2.0' }}"}, + ), ( "vacuum", {"state": "{{ states('vacuum.one') }}"}, @@ -715,6 +731,16 @@ async def test_config_flow_device( {}, "value_template", ), + ( + "update", + {"installed_version": "{{ states('update.one') }}"}, + {"installed_version": "{{ states('update.two') }}"}, + ["off", "on"], + {"one": "2.0", "two": "1.0"}, + {"latest_version": "{{ '2.0' }}"}, + {"latest_version": "{{ '2.0' }}"}, + "installed_version", + ), ( "vacuum", {"state": "{{ states('vacuum.one') }}"}, @@ -1570,6 +1596,12 @@ async def test_option_flow_sensor_preview_config_entry_removed( {}, {}, ), + ( + "update", + {"installed_version": "{{ states('update.one') }}"}, + {"latest_version": "{{ '2.0' }}"}, + {"latest_version": "{{ '2.0' }}"}, + ), ( "vacuum", {"state": "{{ states('vacuum.one') }}"}, diff --git a/tests/components/template/test_init.py b/tests/components/template/test_init.py index 8efca13a218bc9..a95bf2a633206c 100644 --- a/tests/components/template/test_init.py +++ b/tests/components/template/test_init.py @@ -376,6 +376,18 @@ async def async_yaml_patch_helper(hass: HomeAssistant, filename: str) -> None: "event_types": "{{ ['single', 'double'] }}", }, ), + ( + { + "template_type": "update", + "name": "My template", + "latest_version": "{{ '1.0' }}", + "installed_version": "{{ '1.0' }}", + }, + { + "latest_version": "{{ '1.0' }}", + "installed_version": "{{ '1.0' }}", + }, + ), ], ) async def test_change_device( diff --git a/tests/components/template/test_update.py b/tests/components/template/test_update.py new file mode 100644 index 00000000000000..61fbfeede7a952 --- /dev/null +++ b/tests/components/template/test_update.py @@ -0,0 +1,1085 @@ +"""The tests for the Template update platform.""" + +from typing import Any + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components import template, update +from homeassistant.const import ( + ATTR_ENTITY_PICTURE, + ATTR_ICON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant, ServiceCall, State +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.setup import async_setup_component + +from .conftest import ( + ConfigurationStyle, + async_get_flow_preview_state, + async_setup_modern_state_format, + async_setup_modern_trigger_format, + make_test_trigger, +) + +from tests.common import ( + MockConfigEntry, + assert_setup_component, + mock_restore_cache_with_extra_data, +) +from tests.conftest import WebSocketGenerator + +TEST_OBJECT_ID = "template_update" +TEST_ENTITY_ID = f"update.{TEST_OBJECT_ID}" +TEST_INSTALLED_SENSOR = "sensor.installed_update" +TEST_LATEST_SENSOR = "sensor.latest_update" +TEST_SENSOR_ID = "sensor.test_update" +TEST_STATE_TRIGGER = make_test_trigger( + TEST_INSTALLED_SENSOR, TEST_LATEST_SENSOR, TEST_SENSOR_ID +) +TEST_INSTALLED_TEMPLATE = "{{ '1.0' }}" +TEST_LATEST_TEMPLATE = "{{ '2.0' }}" + +TEST_UPDATE_CONFIG = { + "installed_version": TEST_INSTALLED_TEMPLATE, + "latest_version": TEST_LATEST_TEMPLATE, +} +TEST_UNIQUE_ID_CONFIG = { + **TEST_UPDATE_CONFIG, + "unique_id": "not-so-unique-anymore", +} + +INSTALL_ACTION = { + "install": { + "action": "test.automation", + "data": { + "caller": "{{ this.entity_id }}", + "action": "install", + "backup": "{{ backup }}", + "specific_version": "{{ specific_version }}", + }, + } +} + + +async def async_setup_config( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + config: dict[str, Any], + extra_config: dict[str, Any] | None, +) -> None: + """Do setup of update integration.""" + config = {**config, **extra_config} if extra_config else config + if style == ConfigurationStyle.MODERN: + await async_setup_modern_state_format(hass, update.DOMAIN, count, config) + elif style == ConfigurationStyle.TRIGGER: + await async_setup_modern_trigger_format( + hass, update.DOMAIN, TEST_STATE_TRIGGER, count, config + ) + + +@pytest.fixture +async def setup_base( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + config: dict[str, Any], +) -> None: + """Do setup of update integration.""" + await async_setup_config( + hass, + count, + style, + config, + None, + ) + + +@pytest.fixture +async def setup_update( + hass: HomeAssistant, + count: int, + style: ConfigurationStyle, + installed_template: str, + latest_template: str, + extra_config: dict[str, Any] | None, +) -> None: + """Do setup of update integration.""" + await async_setup_config( + hass, + count, + style, + { + "name": TEST_OBJECT_ID, + "installed_version": installed_template, + "latest_version": latest_template, + }, + extra_config, + ) + + +@pytest.fixture +async def setup_single_attribute_update( + hass: HomeAssistant, + style: ConfigurationStyle, + installed_template: str, + latest_template: str, + attribute: str, + attribute_template: str, +) -> None: + """Do setup of update platform testing a single attribute.""" + await async_setup_config( + hass, + 1, + style, + { + "name": TEST_OBJECT_ID, + "installed_version": installed_template, + "latest_version": latest_template, + }, + {attribute: attribute_template} if attribute and attribute_template else {}, + ) + + +async def test_legacy_platform_config(hass: HomeAssistant) -> None: + """Test a legacy platform does not create update entities.""" + with assert_setup_component(1, update.DOMAIN): + assert await async_setup_component( + hass, + update.DOMAIN, + {"update": {"platform": "template", "updates": {TEST_OBJECT_ID: {}}}}, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + assert hass.states.async_all("update") == [] + + +async def test_setup_config_entry( + hass: HomeAssistant, + snapshot: SnapshotAssertion, +) -> None: + """Test the config flow.""" + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": TEST_OBJECT_ID, + "template_type": update.DOMAIN, + **TEST_UPDATE_CONFIG, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state == snapshot + + +async def test_device_id( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test for device for Template.""" + + device_config_entry = MockConfigEntry() + device_config_entry.add_to_hass(hass) + device_entry = device_registry.async_get_or_create( + config_entry_id=device_config_entry.entry_id, + identifiers={("test", "identifier_test")}, + connections={("mac", "30:31:32:33:34:35")}, + ) + await hass.async_block_till_done() + assert device_entry is not None + assert device_entry.id is not None + + template_config_entry = MockConfigEntry( + data={}, + domain=template.DOMAIN, + options={ + "name": TEST_OBJECT_ID, + "template_type": update.DOMAIN, + **TEST_UPDATE_CONFIG, + "device_id": device_entry.id, + }, + title="My template", + ) + template_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(template_config_entry.entry_id) + await hass.async_block_till_done() + + template_entity = entity_registry.async_get(TEST_ENTITY_ID) + assert template_entity is not None + assert template_entity.device_id == device_entry.id + + +@pytest.mark.parametrize(("count", "extra_config"), [(1, None)]) +@pytest.mark.parametrize( + ("style", "expected_state"), + [ + (ConfigurationStyle.MODERN, STATE_UNKNOWN), + (ConfigurationStyle.TRIGGER, STATE_UNKNOWN), + ], +) +@pytest.mark.parametrize( + ("installed_template", "latest_template"), + [ + ("{{states.test['big.fat...']}}", TEST_LATEST_TEMPLATE), + (TEST_INSTALLED_TEMPLATE, "{{states.test['big.fat...']}}"), + ("{{states.test['big.fat...']}}", "{{states.test['big.fat...']}}"), + ], +) +@pytest.mark.usefixtures("setup_update") +async def test_syntax_error( + hass: HomeAssistant, + expected_state: str, +) -> None: + """Test template update with render error.""" + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == expected_state + + +@pytest.mark.parametrize( + ("count", "extra_config", "installed_template", "latest_template"), + [ + ( + 1, + None, + "{{ states('sensor.installed_update') }}", + "{{ states('sensor.latest_update') }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("installed", "latest", "expected"), + [ + ("1.0", "2.0", STATE_ON), + ("2.0", "2.0", STATE_OFF), + ], +) +@pytest.mark.usefixtures("setup_update") +async def test_update_templates( + hass: HomeAssistant, installed: str, latest: str, expected: str +) -> None: + """Test update template.""" + hass.states.async_set(TEST_INSTALLED_SENSOR, installed) + hass.states.async_set(TEST_LATEST_SENSOR, latest) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == expected + assert state.attributes["installed_version"] == installed + assert state.attributes["latest_version"] == latest + + # ensure that the entity picture exists when not provided. + assert ( + state.attributes["entity_picture"] + == "https://brands.home-assistant.io/_/template/icon.png" + ) + + +@pytest.mark.parametrize( + ("count", "extra_config", "installed_template", "latest_template"), + [ + ( + 1, + None, + "{{ states('sensor.installed_update') }}", + "{{ states('sensor.latest_update') }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_update") +async def test_installed_and_latest_template_updates_from_entity( + hass: HomeAssistant, +) -> None: + """Test template installed and latest version templates updates from entities.""" + hass.states.async_set(TEST_INSTALLED_SENSOR, "1.0") + hass.states.async_set(TEST_LATEST_SENSOR, "2.0") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "1.0" + assert state.attributes["latest_version"] == "2.0" + + hass.states.async_set(TEST_INSTALLED_SENSOR, "2.0") + hass.states.async_set(TEST_LATEST_SENSOR, "2.0") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == STATE_OFF + assert state.attributes["installed_version"] == "2.0" + assert state.attributes["latest_version"] == "2.0" + + hass.states.async_set(TEST_INSTALLED_SENSOR, "2.0") + hass.states.async_set(TEST_LATEST_SENSOR, "3.0") + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == STATE_ON + assert state.attributes["installed_version"] == "2.0" + assert state.attributes["latest_version"] == "3.0" + + +@pytest.mark.parametrize( + ("count", "extra_config", "latest_template"), + [(1, None, TEST_LATEST_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("installed_template", "expected", "expected_attr"), + [ + ("{{ '1.0' }}", STATE_ON, "1.0"), + ("{{ 1.0 }}", STATE_ON, "1.0"), + ("{{ '2.0' }}", STATE_OFF, "2.0"), + ("{{ 2.0 }}", STATE_OFF, "2.0"), + ("{{ None }}", STATE_UNKNOWN, None), + ("{{ 'foo' }}", STATE_ON, "foo"), + ("{{ x + 2 }}", STATE_UNKNOWN, None), + ], +) +@pytest.mark.usefixtures("setup_update") +async def test_installed_version_template( + hass: HomeAssistant, expected: str, expected_attr: Any +) -> None: + """Test installed_version template results.""" + # Ensure trigger based template entities update + hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == expected + assert state.attributes["installed_version"] == expected_attr + + +@pytest.mark.parametrize( + ("count", "extra_config", "installed_template"), + [(1, None, TEST_INSTALLED_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("latest_template", "expected", "expected_attr"), + [ + ("{{ '1.0' }}", STATE_OFF, "1.0"), + ("{{ 1.0 }}", STATE_OFF, "1.0"), + ("{{ '2.0' }}", STATE_ON, "2.0"), + ("{{ 2.0 }}", STATE_ON, "2.0"), + ("{{ None }}", STATE_UNKNOWN, None), + ("{{ 'foo' }}", STATE_ON, "foo"), + ("{{ x + 2 }}", STATE_UNKNOWN, None), + ], +) +@pytest.mark.usefixtures("setup_update") +async def test_latest_version_template( + hass: HomeAssistant, expected: str, expected_attr: Any +) -> None: + """Test latest_version template results.""" + # Ensure trigger based template entities update + hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state is not None + assert state.state == expected + assert state.attributes["latest_version"] == expected_attr + + +@pytest.mark.parametrize( + ("count", "extra_config", "installed_template", "latest_template"), + [ + ( + 1, + INSTALL_ACTION, + "{{ states('sensor.installed_update') }}", + "{{ states('sensor.latest_update') }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_update") +async def test_install_action(hass: HomeAssistant, calls: list[ServiceCall]) -> None: + """Test install action.""" + + hass.states.async_set(TEST_INSTALLED_SENSOR, "1.0") + hass.states.async_set(TEST_LATEST_SENSOR, "2.0") + await hass.async_block_till_done() + + await hass.services.async_call( + update.DOMAIN, + update.SERVICE_INSTALL, + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + # verify + assert len(calls) == 1 + assert calls[-1].data["action"] == "install" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + + hass.states.async_set(TEST_INSTALLED_SENSOR, "2.0") + hass.states.async_set(TEST_LATEST_SENSOR, "2.0") + await hass.async_block_till_done() + + # Ensure an error is raised when there's no update. + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + update.DOMAIN, + update.SERVICE_INSTALL, + {"entity_id": TEST_ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + # verify + assert len(calls) == 1 + assert calls[-1].data["action"] == "install" + assert calls[-1].data["caller"] == TEST_ENTITY_ID + + +@pytest.mark.parametrize( + ("installed_template", "latest_template"), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("attribute", "attribute_template", "key", "expected"), + [ + ( + "picture", + "{% if is_state('sensor.installed_update', 'on') %}something{% endif %}", + ATTR_ENTITY_PICTURE, + "something", + ), + ( + "icon", + "{% if is_state('sensor.installed_update', 'on') %}mdi:something{% endif %}", + ATTR_ICON, + "mdi:something", + ), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_entity_picture_and_icon_templates( + hass: HomeAssistant, key: str, expected: str +) -> None: + """Test picture and icon template.""" + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(key) in ("", None) + + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + + assert state.attributes[key] == expected + + +@pytest.mark.parametrize( + ("installed_template", "latest_template"), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("attribute", "attribute_template"), + [ + ( + "picture", + "{{ 'foo.png' if is_state('sensor.installed_update', 'on') else None }}", + ), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_entity_picture_uses_default(hass: HomeAssistant) -> None: + """Test entity picture when template resolves None.""" + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes[ATTR_ENTITY_PICTURE] == "foo.png" + + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + + assert ( + state.attributes[ATTR_ENTITY_PICTURE] + == "https://brands.home-assistant.io/_/template/icon.png" + ) + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute"), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE, "in_progress")], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("attribute_template", "expected", "error"), + [ + ("{{ True }}", True, None), + ("{{ False }}", False, None), + ("{{ None }}", False, "Received invalid in_process value: None"), + ( + "{{ 'foo' }}", + False, + "Received invalid in_process value: foo", + ), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_in_process_template( + hass: HomeAssistant, + attribute: str, + expected: Any, + error: str | None, + caplog: pytest.LogCaptureFixture, + caplog_setup_text: str, +) -> None: + """Test in process templates.""" + # Ensure trigger entities trigger. + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(attribute) == expected + + assert error is None or error in caplog_setup_text or error in caplog.text + + +@pytest.mark.parametrize( + ( + "installed_template", + "latest_template", + ), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize("attribute", ["release_summary", "title"]) +@pytest.mark.parametrize( + ("attribute_template", "expected"), + [ + ("{{ True }}", "True"), + ("{{ False }}", "False"), + ("{{ None }}", None), + ("{{ 'foo' }}", "foo"), + ("{{ 1.0 }}", "1.0"), + ("{{ x + 2 }}", None), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_release_summary_and_title_templates( + hass: HomeAssistant, + attribute: str, + expected: Any, +) -> None: + """Test release summary and title templates.""" + # Ensure trigger entities trigger. + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(attribute) == expected + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute"), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE, "release_url")], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("attribute_template", "expected", "error"), + [ + ("{{ 'http://foo.bar' }}", "http://foo.bar", None), + ("{{ 'https://foo.bar' }}", "https://foo.bar", None), + ("{{ None }}", None, None), + ( + "{{ '/local/thing' }}", + None, + "Received invalid release_url: /local/thing", + ), + ( + "{{ 'foo' }}", + None, + "Received invalid release_url: foo", + ), + ( + "{{ 1.0 }}", + None, + "Received invalid release_url: 1", + ), + ( + "{{ True }}", + None, + "Received invalid release_url: True", + ), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_release_url_template( + hass: HomeAssistant, + attribute: str, + expected: Any, + error: str | None, + caplog: pytest.LogCaptureFixture, + caplog_setup_text: str, +) -> None: + """Test release url templates.""" + # Ensure trigger entities trigger. + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(attribute) == expected + + assert error is None or error in caplog_setup_text or error in caplog.text + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute"), + [(TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE, "update_percentage")], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ("attribute_template", "expected", "error"), + [ + ("{{ 100 }}", 100, None), + ("{{ 0 }}", 0, None), + ("{{ 45 }}", 45, None), + ("{{ None }}", None, None), + ("{{ -1 }}", None, "Received invalid update_percentage: -1"), + ("{{ 101 }}", None, "Received invalid update_percentage: 101"), + ("{{ 'foo' }}", None, "Received invalid update_percentage: foo"), + ("{{ x - 4 }}", None, "UndefinedError: 'x' is undefined"), + ], +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_update_percent_template( + hass: HomeAssistant, + attribute: str, + expected: Any, + error: str | None, + caplog: pytest.LogCaptureFixture, + caplog_setup_text: str, +) -> None: + """Test update percent templates.""" + # Ensure trigger entities trigger. + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes.get(attribute) == expected + + assert error is None or error in caplog_setup_text or error in caplog.text + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute", "attribute_template"), + [ + ( + TEST_INSTALLED_TEMPLATE, + TEST_LATEST_TEMPLATE, + "update_percentage", + "{% set e = 'sensor.test_update' %}{{ states(e) if e | has_value else None }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_optimistic_in_progress_with_update_percent_template( + hass: HomeAssistant, +) -> None: + """Test optimistic in_progress attribute with update percent templates.""" + # Ensure trigger entities trigger. + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["in_progress"] is False + assert state.attributes["update_percentage"] is None + + for i in range(101): + state = hass.states.async_set(TEST_SENSOR_ID, i) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["in_progress"] is True + assert state.attributes["update_percentage"] == i + + state = hass.states.async_set(TEST_SENSOR_ID, STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["in_progress"] is False + assert state.attributes["update_percentage"] is None + + +@pytest.mark.parametrize( + ( + "count", + "installed_template", + "latest_template", + ), + [(1, TEST_INSTALLED_TEMPLATE, TEST_LATEST_TEMPLATE)], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.parametrize( + ( + "extra_config", + "supported_feature", + "action_data", + "expected_backup", + "expected_version", + ), + [ + ( + {"backup": True, **INSTALL_ACTION}, + update.UpdateEntityFeature.BACKUP | update.UpdateEntityFeature.INSTALL, + {"backup": True}, + True, + None, + ), + ( + {"specific_version": True, **INSTALL_ACTION}, + update.UpdateEntityFeature.SPECIFIC_VERSION + | update.UpdateEntityFeature.INSTALL, + {"version": "v2.0"}, + False, + "v2.0", + ), + ( + {"backup": True, "specific_version": True, **INSTALL_ACTION}, + update.UpdateEntityFeature.SPECIFIC_VERSION + | update.UpdateEntityFeature.BACKUP + | update.UpdateEntityFeature.INSTALL, + {"backup": True, "version": "v2.0"}, + True, + "v2.0", + ), + (INSTALL_ACTION, update.UpdateEntityFeature.INSTALL, {}, False, None), + ], +) +@pytest.mark.usefixtures("setup_update") +async def test_supported_features( + hass: HomeAssistant, + supported_feature: update.UpdateEntityFeature, + action_data: dict, + calls: list[ServiceCall], + expected_backup: bool, + expected_version: str | None, +) -> None: + """Test release summary and title templates.""" + # Ensure trigger entities trigger. + state = hass.states.async_set(TEST_INSTALLED_SENSOR, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.attributes["supported_features"] == supported_feature + + await hass.services.async_call( + update.DOMAIN, + update.SERVICE_INSTALL, + {"entity_id": TEST_ENTITY_ID, **action_data}, + blocking=True, + ) + await hass.async_block_till_done() + + # verify + assert len(calls) == 1 + data = calls[-1].data + assert data["action"] == "install" + assert data["caller"] == TEST_ENTITY_ID + assert data["backup"] == expected_backup + assert data["specific_version"] == expected_version + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute", "attribute_template"), + [ + ( + TEST_INSTALLED_TEMPLATE, + TEST_LATEST_TEMPLATE, + "availability", + "{{ 'sensor.test_update' | has_value }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_available_template_with_entities(hass: HomeAssistant) -> None: + """Test availability templates with values from other entities.""" + hass.states.async_set(TEST_SENSOR_ID, STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state != STATE_UNAVAILABLE + + hass.states.async_set(TEST_SENSOR_ID, STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + hass.states.async_set(TEST_SENSOR_ID, STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state != STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ("installed_template", "latest_template", "attribute", "attribute_template"), + [ + ( + TEST_INSTALLED_TEMPLATE, + TEST_LATEST_TEMPLATE, + "availability", + "{{ x - 12 }}", + ) + ], +) +@pytest.mark.parametrize( + "style", [ConfigurationStyle.MODERN, ConfigurationStyle.TRIGGER] +) +@pytest.mark.usefixtures("setup_single_attribute_update") +async def test_invalid_availability_template_keeps_component_available( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + caplog_setup_text, +) -> None: + """Test that an invalid availability keeps the device available.""" + # Ensure entity triggers + hass.states.async_set(TEST_SENSOR_ID, "anything") + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID).state != STATE_UNAVAILABLE + + error = "UndefinedError: 'x' is undefined" + assert error in caplog_setup_text or error in caplog.text + + +@pytest.mark.parametrize(("count", "domain"), [(1, "template")]) +@pytest.mark.parametrize( + "config", + [ + { + "template": { + "trigger": {"platform": "event", "event_type": "test_event"}, + "update": { + "name": TEST_OBJECT_ID, + "installed_version": "{{ trigger.event.data.action }}", + "latest_version": "{{ '1.0.2' }}", + "picture": "{{ '/local/dogs.png' }}", + "icon": "{{ 'mdi:pirate' }}", + }, + }, + }, + ], +) +async def test_trigger_entity_restore_state( + hass: HomeAssistant, + count: int, + domain: str, + config: dict, +) -> None: + """Test restoring trigger entities.""" + restored_attributes = { + "installed_version": "1.0.0", + "latest_version": "1.0.1", + "entity_picture": "/local/cats.png", + "icon": "mdi:ship", + "skipped_version": "1.0.1", + } + fake_state = State( + TEST_ENTITY_ID, + STATE_OFF, + restored_attributes, + ) + mock_restore_cache_with_extra_data(hass, ((fake_state, {}),)) + with assert_setup_component(count, domain): + assert await async_setup_component( + hass, + domain, + config, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF + for attr, value in restored_attributes.items(): + assert state.attributes[attr] == value + + hass.bus.async_fire("test_event", {"action": "1.0.0"}) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_ON + assert state.attributes["icon"] == "mdi:pirate" + assert state.attributes["entity_picture"] == "/local/dogs.png" + + +@pytest.mark.parametrize("count", [1]) +@pytest.mark.parametrize( + ("updates", "style"), + [ + ( + [ + { + "name": "test_template_event_01", + **TEST_UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_event_02", + **TEST_UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.MODERN, + ), + ( + [ + { + "name": "test_template_event_01", + **TEST_UNIQUE_ID_CONFIG, + }, + { + "name": "test_template_event_02", + **TEST_UNIQUE_ID_CONFIG, + }, + ], + ConfigurationStyle.TRIGGER, + ), + ], +) +async def test_unique_id( + hass: HomeAssistant, count: int, updates: list[dict], style: ConfigurationStyle +) -> None: + """Test unique_id option only creates one update entity per id.""" + config = {"update": updates} + if style == ConfigurationStyle.TRIGGER: + config = {**config, **TEST_STATE_TRIGGER} + with assert_setup_component(count, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + {"template": config}, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("update")) == 1 + + +async def test_nested_unique_id( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: + """Test unique_id option creates one update entity per nested id.""" + + with assert_setup_component(1, template.DOMAIN): + assert await async_setup_component( + hass, + template.DOMAIN, + { + "template": { + "unique_id": "x", + "update": [ + { + "name": "test_a", + **TEST_UPDATE_CONFIG, + "unique_id": "a", + }, + { + "name": "test_b", + **TEST_UPDATE_CONFIG, + "unique_id": "b", + }, + ], + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + assert len(hass.states.async_all("update")) == 2 + + entry = entity_registry.async_get("update.test_a") + assert entry + assert entry.unique_id == "x-a" + + entry = entity_registry.async_get("update.test_b") + assert entry + assert entry.unique_id == "x-b" + + +async def test_flow_preview( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test the config flow preview.""" + + state = await async_get_flow_preview_state( + hass, + hass_ws_client, + update.DOMAIN, + {"name": "My template", **TEST_UPDATE_CONFIG}, + ) + + assert state["state"] == STATE_ON + assert state["attributes"]["installed_version"] == "1.0" + assert state["attributes"]["latest_version"] == "2.0" From dea5e7454ace056f0ff1848c36b9e59bf99c6919 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Mon, 25 Aug 2025 15:37:36 +0200 Subject: [PATCH 3/5] Add MQTT lock subentry support (#150860) Co-authored-by: Norbert Rittel --- homeassistant/components/mqtt/config_flow.py | 106 +++++++++++++++++++ homeassistant/components/mqtt/const.py | 18 ++++ homeassistant/components/mqtt/lock.py | 43 ++++---- homeassistant/components/mqtt/strings.json | 29 +++++ tests/components/mqtt/common.py | 29 +++++ tests/components/mqtt/test_config_flow.py | 51 +++++++++ 6 files changed, 252 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 03f758dbdcecd1..a8a4c2e95384ce 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -149,6 +149,7 @@ CONF_CERTIFICATE, CONF_CLIENT_CERT, CONF_CLIENT_KEY, + CONF_CODE_FORMAT, CONF_COLOR_MODE_STATE_TOPIC, CONF_COLOR_MODE_VALUE_TEMPLATE, CONF_COLOR_TEMP_COMMAND_TEMPLATE, @@ -217,15 +218,18 @@ CONF_OSCILLATION_VALUE_TEMPLATE, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_CLOSE, + CONF_PAYLOAD_LOCK, CONF_PAYLOAD_NOT_AVAILABLE, CONF_PAYLOAD_OPEN, CONF_PAYLOAD_OSCILLATION_OFF, CONF_PAYLOAD_OSCILLATION_ON, CONF_PAYLOAD_PRESS, + CONF_PAYLOAD_RESET, CONF_PAYLOAD_RESET_PERCENTAGE, CONF_PAYLOAD_RESET_PRESET_MODE, CONF_PAYLOAD_STOP, CONF_PAYLOAD_STOP_TILT, + CONF_PAYLOAD_UNLOCK, CONF_PERCENTAGE_COMMAND_TEMPLATE, CONF_PERCENTAGE_COMMAND_TOPIC, CONF_PERCENTAGE_STATE_TOPIC, @@ -262,12 +266,17 @@ CONF_SPEED_RANGE_MIN, CONF_STATE_CLOSED, CONF_STATE_CLOSING, + CONF_STATE_JAMMED, + CONF_STATE_LOCKED, + CONF_STATE_LOCKING, CONF_STATE_OFF, CONF_STATE_ON, CONF_STATE_OPEN, CONF_STATE_OPENING, CONF_STATE_STOPPED, CONF_STATE_TOPIC, + CONF_STATE_UNLOCKED, + CONF_STATE_UNLOCKING, CONF_STATE_VALUE_TEMPLATE, CONF_SUGGESTED_DISPLAY_PRECISION, CONF_SUPPORTED_COLOR_MODES, @@ -328,6 +337,7 @@ DEFAULT_ON_COMMAND_TYPE, DEFAULT_PAYLOAD_AVAILABLE, DEFAULT_PAYLOAD_CLOSE, + DEFAULT_PAYLOAD_LOCK, DEFAULT_PAYLOAD_NOT_AVAILABLE, DEFAULT_PAYLOAD_OFF, DEFAULT_PAYLOAD_ON, @@ -337,6 +347,7 @@ DEFAULT_PAYLOAD_PRESS, DEFAULT_PAYLOAD_RESET, DEFAULT_PAYLOAD_STOP, + DEFAULT_PAYLOAD_UNLOCK, DEFAULT_PORT, DEFAULT_POSITION_CLOSED, DEFAULT_POSITION_OPEN, @@ -345,7 +356,12 @@ DEFAULT_QOS, DEFAULT_SPEED_RANGE_MAX, DEFAULT_SPEED_RANGE_MIN, + DEFAULT_STATE_JAMMED, + DEFAULT_STATE_LOCKED, + DEFAULT_STATE_LOCKING, DEFAULT_STATE_STOPPED, + DEFAULT_STATE_UNLOCKED, + DEFAULT_STATE_UNLOCKING, DEFAULT_TILT_CLOSED_POSITION, DEFAULT_TILT_MAX, DEFAULT_TILT_MIN, @@ -458,6 +474,7 @@ Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH, @@ -1148,6 +1165,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]: is_schema_default=True, ), }, + Platform.LOCK.value: {}, } PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = { Platform.BINARY_SENSOR.value: { @@ -2664,6 +2682,93 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]: section="advanced_settings", ), }, + Platform.LOCK.value: { + CONF_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + ), + CONF_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + ), + CONF_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_CODE_FORMAT: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=validate(cv.is_regex), + error="invalid_regular_expression", + ), + CONF_PAYLOAD_LOCK: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_LOCK, + section="lock_payload_settings", + ), + CONF_PAYLOAD_UNLOCK: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_UNLOCK, + section="lock_payload_settings", + ), + CONF_PAYLOAD_OPEN: PlatformField( + selector=TEXT_SELECTOR, + required=False, + section="lock_payload_settings", + ), + CONF_PAYLOAD_RESET: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_PAYLOAD_RESET, + section="lock_payload_settings", + ), + CONF_STATE_LOCKED: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_LOCKED, + section="lock_payload_settings", + ), + CONF_STATE_UNLOCKED: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_UNLOCKED, + section="lock_payload_settings", + ), + CONF_STATE_LOCKING: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_LOCKING, + section="lock_payload_settings", + ), + CONF_STATE_UNLOCKING: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_UNLOCKING, + section="lock_payload_settings", + ), + CONF_STATE_JAMMED: PlatformField( + selector=TEXT_SELECTOR, + required=False, + default=DEFAULT_STATE_JAMMED, + section="lock_payload_settings", + ), + CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + }, } ENTITY_CONFIG_VALIDATOR: dict[ str, @@ -2675,6 +2780,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]: Platform.COVER.value: validate_cover_platform_config, Platform.FAN.value: validate_fan_platform_config, Platform.LIGHT.value: validate_light_platform_config, + Platform.LOCK.value: None, Platform.NOTIFY.value: None, Platform.SENSOR.value: validate_sensor_platform_config, Platform.SWITCH.value: None, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 1dfdb8dac53b61..2128b55c4b0a78 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -31,6 +31,7 @@ CONF_AVAILABILITY_TOPIC = "availability_topic" CONF_BROKER = "broker" CONF_BIRTH_MESSAGE = "birth_message" +CONF_CODE_FORMAT = "code_format" CONF_COMMAND_TEMPLATE = "command_template" CONF_COMMAND_TOPIC = "command_topic" CONF_DISCOVERY_PREFIX = "discovery_prefix" @@ -127,6 +128,7 @@ CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" CONF_PAYLOAD_CLOSE = "payload_close" +CONF_PAYLOAD_LOCK = "payload_lock" CONF_PAYLOAD_OPEN = "payload_open" CONF_PAYLOAD_OSCILLATION_OFF = "payload_oscillation_off" CONF_PAYLOAD_OSCILLATION_ON = "payload_oscillation_on" @@ -135,6 +137,7 @@ CONF_PAYLOAD_RESET_PRESET_MODE = "payload_reset_preset_mode" CONF_PAYLOAD_STOP = "payload_stop" CONF_PAYLOAD_STOP_TILT = "payload_stop_tilt" +CONF_PAYLOAD_UNLOCK = "payload_unlock" CONF_PERCENTAGE_COMMAND_TEMPLATE = "percentage_command_template" CONF_PERCENTAGE_COMMAND_TOPIC = "percentage_command_topic" CONF_PERCENTAGE_STATE_TOPIC = "percentage_state_topic" @@ -168,11 +171,16 @@ CONF_SPEED_RANGE_MIN = "speed_range_min" CONF_STATE_CLOSED = "state_closed" CONF_STATE_CLOSING = "state_closing" +CONF_STATE_JAMMED = "state_jammed" +CONF_STATE_LOCKED = "state_locked" +CONF_STATE_LOCKING = "state_locking" CONF_STATE_OFF = "state_off" CONF_STATE_ON = "state_on" CONF_STATE_OPEN = "state_open" CONF_STATE_OPENING = "state_opening" CONF_STATE_STOPPED = "state_stopped" +CONF_STATE_UNLOCKED = "state_unlocked" +CONF_STATE_UNLOCKING = "state_unlocking" CONF_SUGGESTED_DISPLAY_PRECISION = "suggested_display_precision" CONF_SUPPORTED_COLOR_MODES = "supported_color_modes" CONF_SWING_HORIZONTAL_MODE_COMMAND_TEMPLATE = "swing_horizontal_mode_command_template" @@ -254,6 +262,7 @@ DEFAULT_QOS = 0 DEFAULT_PAYLOAD_AVAILABLE = "online" DEFAULT_PAYLOAD_CLOSE = "CLOSE" +DEFAULT_PAYLOAD_LOCK = "LOCK" DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" DEFAULT_PAYLOAD_OFF = "OFF" DEFAULT_PAYLOAD_ON = "ON" @@ -263,6 +272,8 @@ DEFAULT_PAYLOAD_PRESS = "PRESS" DEFAULT_PAYLOAD_STOP = "STOP" DEFAULT_PAYLOAD_RESET = "None" +DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" + DEFAULT_PORT = 1883 DEFAULT_RETAIN = False DEFAULT_TILT_CLOSED_POSITION = 0 @@ -277,7 +288,14 @@ DEFAULT_RETAIN = False DEFAULT_SPEED_RANGE_MAX = 100 DEFAULT_SPEED_RANGE_MIN = 1 +DEFAULT_STATE_LOCKED = "LOCKED" +DEFAULT_STATE_LOCKING = "LOCKING" +DEFAULT_STATE_OPEN = "OPEN" +DEFAULT_STATE_OPENING = "OPENING" DEFAULT_STATE_STOPPED = "stopped" +DEFAULT_STATE_UNLOCKED = "UNLOCKED" +DEFAULT_STATE_UNLOCKING = "UNLOCKING" +DEFAULT_STATE_JAMMED = "JAMMED" DEFAULT_WHITE_SCALE = 255 COVER_PAYLOAD = "cover" diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 727e689798e66f..00771ce521f6e1 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -27,12 +27,31 @@ from . import subscription from .config import MQTT_RW_SCHEMA from .const import ( + CONF_CODE_FORMAT, CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, + CONF_PAYLOAD_LOCK, + CONF_PAYLOAD_OPEN, CONF_PAYLOAD_RESET, + CONF_PAYLOAD_UNLOCK, + CONF_STATE_JAMMED, + CONF_STATE_LOCKED, + CONF_STATE_LOCKING, CONF_STATE_OPEN, CONF_STATE_OPENING, CONF_STATE_TOPIC, + CONF_STATE_UNLOCKED, + CONF_STATE_UNLOCKING, + DEFAULT_PAYLOAD_LOCK, + DEFAULT_PAYLOAD_RESET, + DEFAULT_PAYLOAD_UNLOCK, + DEFAULT_STATE_JAMMED, + DEFAULT_STATE_LOCKED, + DEFAULT_STATE_LOCKING, + DEFAULT_STATE_OPEN, + DEFAULT_STATE_OPENING, + DEFAULT_STATE_UNLOCKED, + DEFAULT_STATE_UNLOCKING, ) from .entity import MqttEntity, async_setup_entity_entry_helper from .models import ( @@ -47,31 +66,7 @@ PARALLEL_UPDATES = 0 -CONF_CODE_FORMAT = "code_format" - -CONF_PAYLOAD_LOCK = "payload_lock" -CONF_PAYLOAD_UNLOCK = "payload_unlock" -CONF_PAYLOAD_OPEN = "payload_open" - -CONF_STATE_LOCKED = "state_locked" -CONF_STATE_LOCKING = "state_locking" - -CONF_STATE_UNLOCKED = "state_unlocked" -CONF_STATE_UNLOCKING = "state_unlocking" -CONF_STATE_JAMMED = "state_jammed" - DEFAULT_NAME = "MQTT Lock" -DEFAULT_PAYLOAD_LOCK = "LOCK" -DEFAULT_PAYLOAD_UNLOCK = "UNLOCK" -DEFAULT_PAYLOAD_OPEN = "OPEN" -DEFAULT_PAYLOAD_RESET = "None" -DEFAULT_STATE_LOCKED = "LOCKED" -DEFAULT_STATE_LOCKING = "LOCKING" -DEFAULT_STATE_OPEN = "OPEN" -DEFAULT_STATE_OPENING = "OPENING" -DEFAULT_STATE_UNLOCKED = "UNLOCKED" -DEFAULT_STATE_UNLOCKING = "UNLOCKING" -DEFAULT_STATE_JAMMED = "JAMMED" MQTT_LOCK_ATTRIBUTES_BLOCKED = frozenset( { diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 77a476bf40cc72..3844cf8d669f3b 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -308,6 +308,7 @@ "data": { "blue_template": "Blue template", "brightness_template": "Brightness template", + "code_format": "Code format", "command_template": "Command template", "command_topic": "Command topic", "command_off_template": "Command \"off\" template", @@ -340,6 +341,7 @@ "data_description": { "blue_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract blue color from the state payload value. Expected result of the template is an integer from 0-255 range.", "brightness_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract brightness from the state payload value. Expected result of the template is an integer from 0-255 range.", + "code_format": "A regular expression to validate a supplied code when it is set during the action to open, lock or unlock the MQTT lock. [Learn more.]({url}#code_format)", "command_off_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"off\" state changes. Available variables are: `state` and `transition`.", "command_on_template": "The [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) for \"on\" state changes. Available variables: `state`, `brightness`, `color_temp`, `red`, `green`, `blue`, `hue`, `sat`, `flash`, `transition` and `effect`. Values `red`, `green`, `blue` and `brightness` are provided as integers from range 0-255. Value of `hue` is provided as float from range 0-360. Value of `sat` is provided as float from range 0-100. Value of `color_temp` is provided as integer representing Kelvin units.", "command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic.", @@ -596,6 +598,31 @@ "brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value." } }, + "lock_payload_settings": { + "name": "Lock payload settings", + "data": { + "payload_lock": "Payload \"lock\"", + "payload_open": "Payload \"open\"", + "payload_reset": "Payload \"reset\"", + "payload_unlock": "Payload \"unlock\"", + "state_jammed": "State \"jammed\"", + "state_locked": "State \"locked\"", + "state_locking": "State \"locking\"", + "state_unlocked": "State \"unlocked\"", + "state_unlocking": "State \"unlocking\"" + }, + "data_description": { + "payload_lock": "The payload sent when a \"lock\" command is issued.", + "payload_open": "The payload sent when an \"open\" command is issued. Set this payload if your lock supports the \"open\" action.", + "payload_reset": "The payload received at the state topic that resets the lock to an unknown state.", + "payload_unlock": "The payload sent when an \"unlock\" command is issued.", + "state_jammed": "The payload received at the state topic that represents the \"jammed\" state.", + "state_locked": "The payload received at the state topic that represents the \"locked\" state.", + "state_locking": "The payload received at the state topic that represents the \"locking\" state.", + "state_unlocked": "The payload received at the state topic that represents the \"unlocked\" state.", + "state_unlocking": "The payload received at the state topic that represents the \"unlocking\" state." + } + }, "fan_direction_settings": { "name": "Direction settings", "data": { @@ -911,6 +938,7 @@ "fan_speed_range_max_must_be_greater_than_speed_range_min": "Speed range max must be greater than speed range min", "fan_preset_mode_reset_in_preset_modes_list": "Payload \"reset preset mode\" is not a valid as a preset mode", "invalid_input": "Invalid value", + "invalid_regular_expression": "Must be a valid regular expression", "invalid_subscribe_topic": "Invalid subscribe topic", "invalid_template": "Invalid template", "invalid_supported_color_modes": "Invalid supported color modes selection", @@ -1201,6 +1229,7 @@ "cover": "[%key:component::cover::title%]", "fan": "[%key:component::fan::title%]", "light": "[%key:component::light::title%]", + "lock": "[%key:component::lock::title%]", "notify": "[%key:component::notify::title%]", "sensor": "[%key:component::sensor::title%]", "switch": "[%key:component::switch::title%]" diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index fdaed0c323f47a..b3a93ec0cf201e 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -316,6 +316,31 @@ }, } +MOCK_SUBENTRY_LOCK_COMPONENT = { + "3faf1318016c46c5aea26707eeb6f100": { + "platform": "lock", + "name": "Lock", + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{ value }}", + "value_template": "{{ value_json.value }}", + "code_format": "^\\d{4}$", + "payload_open": "OPEN", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_reset": "None", + "state_jammed": "JAMMED", + "state_locked": "LOCKED", + "state_locking": "LOCKING", + "state_unlocked": "UNLOCKED", + "state_unlocking": "UNLOCKING", + "retain": False, + "entity_category": None, + "entity_picture": "https://example.com/3faf1318016c46c5aea26707eeb6f100", + "optimistic": True, + }, +} + MOCK_SUBENTRY_SENSOR_COMPONENT = { "e9261f6feed443e7b7d5f3fbe2a47412": { "platform": "sensor", @@ -459,6 +484,10 @@ "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_LIGHT_BASIC_KELVIN_COMPONENT, } +MOCK_LOCK_SUBENTRY_DATA_SINGLE = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, + "components": MOCK_SUBENTRY_LOCK_COMPONENT, +} MOCK_SENSOR_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_SENSOR_COMPONENT, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 3b4f090aef33fb..1c99d9da45fccf 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -41,6 +41,7 @@ MOCK_COVER_SUBENTRY_DATA_SINGLE, MOCK_FAN_SUBENTRY_DATA_SINGLE, MOCK_LIGHT_BASIC_KELVIN_SUBENTRY_DATA_SINGLE, + MOCK_LOCK_SUBENTRY_DATA_SINGLE, MOCK_NOTIFY_SUBENTRY_DATA_MULTI, MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME, MOCK_NOTIFY_SUBENTRY_DATA_SINGLE, @@ -3347,6 +3348,55 @@ async def test_migrate_of_incompatible_config_entry( ), "Milk notifier Basic light", ), + ( + MOCK_LOCK_SUBENTRY_DATA_SINGLE, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Lock"}, + {}, + (), + { + "command_topic": "test-topic", + "command_template": "{{ value }}", + "state_topic": "test-topic", + "value_template": "{{ value_json.value }}", + "code_format": "^\\d{4}$", + "optimistic": True, + "retain": False, + "lock_payload_settings": { + "payload_open": "OPEN", + "payload_lock": "LOCK", + "payload_unlock": "UNLOCK", + "payload_reset": "None", + "state_jammed": "JAMMED", + "state_locked": "LOCKED", + "state_locking": "LOCKING", + "state_unlocked": "UNLOCKED", + "state_unlocking": "UNLOCKING", + }, + }, + ( + ( + {"command_topic": "test-topic#invalid"}, + {"command_topic": "invalid_publish_topic"}, + ), + ( + { + "command_topic": "test-topic", + "state_topic": "test-topic#invalid", + }, + {"state_topic": "invalid_subscribe_topic"}, + ), + ( + { + "command_topic": "test-topic", + "code_format": "(", + }, + {"code_format": "invalid_regular_expression"}, + ), + ), + "Milk notifier Lock", + ), + # MOCK_LOCK_SUBENTRY_DATA_SINGLE ], ids=[ "binary_sensor", @@ -3362,6 +3412,7 @@ async def test_migrate_of_incompatible_config_entry( "sensor_total", "switch", "light_basic_kelvin", + "lock", ], ) async def test_subentry_configflow( From 8b29e3011e8d7fe79c2a1bfe551ae48ab708de84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludovic=20BOU=C3=89?= Date: Mon, 25 Aug 2025 15:43:00 +0200 Subject: [PATCH 4/5] Matter Valve new attributes (#150788) --- .../components/matter/binary_sensor.py | 53 +++++++ homeassistant/components/matter/number.py | 17 ++ homeassistant/components/matter/sensor.py | 15 ++ homeassistant/components/matter/strings.json | 15 ++ homeassistant/components/matter/valve.py | 1 - .../matter/snapshots/test_binary_sensor.ambr | 147 ++++++++++++++++++ .../matter/snapshots/test_number.ambr | 58 +++++++ tests/components/matter/test_binary_sensor.py | 69 ++++++++ 8 files changed, 374 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index ea74baab773171..b36e826e711e65 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -407,6 +407,59 @@ def _update_from_device(self) -> None: required_attributes=(clusters.DishwasherAlarm.Attributes.State,), allow_multi=True, ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="ValveConfigurationAndControlValveFault_GeneralFault", + translation_key="valve_fault_general_fault", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + device_to_ha=lambda x: ( + x + == clusters.ValveConfigurationAndControl.Bitmaps.ValveFaultBitmap.kGeneralFault + ), + ), + entity_class=MatterBinarySensor, + required_attributes=( + clusters.ValveConfigurationAndControl.Attributes.ValveFault, + ), + allow_multi=True, + ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="ValveConfigurationAndControlValveFault_Blocked", + translation_key="valve_fault_blocked", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + device_to_ha=lambda x: ( + x + == clusters.ValveConfigurationAndControl.Bitmaps.ValveFaultBitmap.kBlocked + ), + ), + entity_class=MatterBinarySensor, + required_attributes=( + clusters.ValveConfigurationAndControl.Attributes.ValveFault, + ), + allow_multi=True, + ), + MatterDiscoverySchema( + platform=Platform.BINARY_SENSOR, + entity_description=MatterBinarySensorEntityDescription( + key="ValveConfigurationAndControlValveFault_Leaking", + translation_key="valve_fault_leaking", + device_class=BinarySensorDeviceClass.PROBLEM, + entity_category=EntityCategory.DIAGNOSTIC, + device_to_ha=lambda x: ( + x + == clusters.ValveConfigurationAndControl.Bitmaps.ValveFaultBitmap.kLeaking + ), + ), + entity_class=MatterBinarySensor, + required_attributes=( + clusters.ValveConfigurationAndControl.Attributes.ValveFault, + ), + ), MatterDiscoverySchema( platform=Platform.BINARY_SENSOR, entity_description=MatterBinarySensorEntityDescription( diff --git a/homeassistant/components/matter/number.py b/homeassistant/components/matter/number.py index d2184891dc1be6..4540c5bd2b310a 100644 --- a/homeassistant/components/matter/number.py +++ b/homeassistant/components/matter/number.py @@ -313,6 +313,23 @@ def _update_from_device(self) -> None: clusters.OccupancySensing.Attributes.PIROccupiedToUnoccupiedDelay, ), ), + MatterDiscoverySchema( + platform=Platform.NUMBER, + entity_description=MatterNumberEntityDescription( + key="ValveConfigurationAndControlDefaultOpenDuration", + entity_category=EntityCategory.CONFIG, + translation_key="valve_configuration_and_control_default_open_duration", + native_max_value=65534, + native_min_value=1, + native_unit_of_measurement=UnitOfTime.SECONDS, + mode=NumberMode.BOX, + ), + entity_class=MatterNumber, + required_attributes=( + clusters.ValveConfigurationAndControl.Attributes.DefaultOpenDuration, + ), + allow_multi=True, + ), MatterDiscoverySchema( platform=Platform.NUMBER, entity_description=MatterRangeNumberEntityDescription( diff --git a/homeassistant/components/matter/sensor.py b/homeassistant/components/matter/sensor.py index 18bd7f84da33f6..d8e55b7b1ff9da 100644 --- a/homeassistant/components/matter/sensor.py +++ b/homeassistant/components/matter/sensor.py @@ -1370,4 +1370,19 @@ def _update_from_device(self) -> None: entity_class=MatterSensor, required_attributes=(clusters.PumpConfigurationAndControl.Attributes.Speed,), ), + MatterDiscoverySchema( + platform=Platform.SENSOR, + entity_description=MatterSensorEntityDescription( + key="ValveConfigurationAndControlAutoCloseTime", + translation_key="auto_close_time", + device_class=SensorDeviceClass.TIMESTAMP, + state_class=None, + device_to_ha=(lambda x: dt_util.utc_from_timestamp(x) if x > 0 else None), + ), + entity_class=MatterSensor, + featuremap_contains=clusters.ValveConfigurationAndControl.Bitmaps.Feature.kTimeSync, + required_attributes=( + clusters.ValveConfigurationAndControl.Attributes.AutoCloseTime, + ), + ), ] diff --git a/homeassistant/components/matter/strings.json b/homeassistant/components/matter/strings.json index e9c023cd74ef21..9a0bb77adfa7ac 100644 --- a/homeassistant/components/matter/strings.json +++ b/homeassistant/components/matter/strings.json @@ -94,6 +94,15 @@ }, "alarm_door": { "name": "Door alarm" + }, + "valve_fault_blocked": { + "name": "Valve blocked" + }, + "valve_fault_general_fault": { + "name": "General fault" + }, + "valve_fault_leaking": { + "name": "Valve leaking" } }, "button": { @@ -206,6 +215,9 @@ }, "led_indicator_intensity_on": { "name": "LED on intensity" + }, + "valve_configuration_and_control_default_open_duration": { + "name": "Default open duration" } }, "light": { @@ -292,6 +304,9 @@ "activated_carbon_filter_condition": { "name": "Activated carbon filter condition" }, + "auto_close_time": { + "name": "Auto-close time" + }, "contamination_state": { "name": "Contamination state", "state": { diff --git a/homeassistant/components/matter/valve.py b/homeassistant/components/matter/valve.py index 4cedec74bf24b2..715cdc2a09e669 100644 --- a/homeassistant/components/matter/valve.py +++ b/homeassistant/components/matter/valve.py @@ -21,7 +21,6 @@ from .models import MatterDiscoverySchema ValveConfigurationAndControl = clusters.ValveConfigurationAndControl - ValveStateEnum = ValveConfigurationAndControl.Enums.ValveStateEnum diff --git a/tests/components/matter/snapshots/test_binary_sensor.ambr b/tests/components/matter/snapshots/test_binary_sensor.ambr index 8756efdfbd2d56..da199afd3a670d 100644 --- a/tests/components/matter/snapshots/test_binary_sensor.ambr +++ b/tests/components/matter/snapshots/test_binary_sensor.ambr @@ -1124,3 +1124,150 @@ 'state': 'off', }) # --- +# name: test_binary_sensors[valve][binary_sensor.valve_general_fault-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.valve_general_fault', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'General fault', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve_fault_general_fault', + 'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-ValveConfigurationAndControlValveFault_GeneralFault-129-9', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[valve][binary_sensor.valve_general_fault-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Valve General fault', + }), + 'context': , + 'entity_id': 'binary_sensor.valve_general_fault', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[valve][binary_sensor.valve_valve_blocked-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.valve_valve_blocked', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve blocked', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve_fault_blocked', + 'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-ValveConfigurationAndControlValveFault_Blocked-129-9', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[valve][binary_sensor.valve_valve_blocked-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Valve Valve blocked', + }), + 'context': , + 'entity_id': 'binary_sensor.valve_valve_blocked', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_binary_sensors[valve][binary_sensor.valve_valve_leaking-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.valve_valve_leaking', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Valve leaking', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve_fault_leaking', + 'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-ValveConfigurationAndControlValveFault_Leaking-129-9', + 'unit_of_measurement': None, + }) +# --- +# name: test_binary_sensors[valve][binary_sensor.valve_valve_leaking-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Valve Valve leaking', + }), + 'context': , + 'entity_id': 'binary_sensor.valve_valve_leaking', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/matter/snapshots/test_number.ambr b/tests/components/matter/snapshots/test_number.ambr index 24a92799082fbb..0273c83ac5c36d 100644 --- a/tests/components/matter/snapshots/test_number.ambr +++ b/tests/components/matter/snapshots/test_number.ambr @@ -2366,3 +2366,61 @@ 'state': '4.0', }) # --- +# name: test_numbers[valve][number.valve_default_open_duration-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 65534, + 'min': 1, + 'mode': , + 'step': 1.0, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.valve_default_open_duration', + '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': 'Default open duration', + 'platform': 'matter', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'valve_configuration_and_control_default_open_duration', + 'unique_id': '00000000000004D2-000000000000004B-MatterNodeDevice-1-ValveConfigurationAndControlDefaultOpenDuration-129-1', + 'unit_of_measurement': , + }) +# --- +# name: test_numbers[valve][number.valve_default_open_duration-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Valve Default open duration', + 'max': 65534, + 'min': 1, + 'mode': , + 'step': 1.0, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.valve_default_open_duration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index fcfd4da84c8258..06055af8c9dc38 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -275,3 +275,72 @@ async def test_dishwasher_alarm( state = hass.states.get("binary_sensor.dishwasher_door_alarm") assert state assert state.state == "on" + + +@pytest.mark.parametrize("node_fixture", ["valve"]) +async def test_water_valve( + hass: HomeAssistant, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test valve alarms.""" + # ValveFault default state + state = hass.states.get("binary_sensor.valve_general_fault") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_blocked") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_leaking") + assert state + assert state.state == "off" + + # ValveFault general_fault test + set_node_attribute(matter_node, 1, 129, 9, 1) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.valve_general_fault") + assert state + assert state.state == "on" + + state = hass.states.get("binary_sensor.valve_valve_blocked") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_leaking") + assert state + assert state.state == "off" + + # ValveFault valve_blocked test + set_node_attribute(matter_node, 1, 129, 9, 2) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.valve_general_fault") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_blocked") + assert state + assert state.state == "on" + + state = hass.states.get("binary_sensor.valve_valve_leaking") + assert state + assert state.state == "off" + + # ValveFault valve_leaking test + set_node_attribute(matter_node, 1, 129, 9, 4) + await trigger_subscription_callback(hass, matter_client) + + state = hass.states.get("binary_sensor.valve_general_fault") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_blocked") + assert state + assert state.state == "off" + + state = hass.states.get("binary_sensor.valve_valve_leaking") + assert state + assert state.state == "on" From f1d2b102cfec4b50597ff71e1409a23d97f3d4a4 Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Mon, 25 Aug 2025 16:57:04 +0200 Subject: [PATCH 5/5] Fix broken reference for "event_types" in `template` (#151152) --- homeassistant/components/template/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/template/strings.json b/homeassistant/components/template/strings.json index c565023f7de3ad..5b62f6bc8e8484 100644 --- a/homeassistant/components/template/strings.json +++ b/homeassistant/components/template/strings.json @@ -640,7 +640,7 @@ "device_id": "[%key:common::config_flow::data::device%]", "name": "[%key:common::config_flow::data::name%]", "event_type": "[%key:component::template::config::step::event::data::event_type%]", - "event_types": "[%component::event::entity_component::_::state_attributes::event_types::name%]" + "event_types": "[%key:component::event::entity_component::_::state_attributes::event_types::name%]" }, "data_description": { "device_id": "[%key:component::template::common::device_id_description%]",