commit 79938b9f199ed063dcd237f8130a33f2ca8eb305 Author: vlebourl Date: Fri Apr 1 15:02:13 2022 +0200 first commit diff --git a/Config/.HA_VERSION b/Config/.HA_VERSION new file mode 100644 index 0000000..cc872d7 --- /dev/null +++ b/Config/.HA_VERSION @@ -0,0 +1 @@ +2022.3.8 \ No newline at end of file diff --git a/Config/.storage/auth b/Config/.storage/auth new file mode 100644 index 0000000..051203a --- /dev/null +++ b/Config/.storage/auth @@ -0,0 +1,122 @@ +{ + "version": 1, + "minor_version": 1, + "key": "auth", + "data": { + "users": [ + { + "id": "563d33967d924c588c6ecbff5eb96ed8", + "group_ids": [ + "system-read-only" + ], + "is_owner": false, + "is_active": true, + "name": "Home Assistant Content", + "system_generated": true, + "local_only": false + }, + { + "id": "7e27048212fd43d78e900bed6f4ac44c", + "group_ids": [ + "system-admin" + ], + "is_owner": true, + "is_active": true, + "name": "vlb", + "system_generated": false, + "local_only": false + } + ], + "groups": [ + { + "id": "system-admin", + "name": "Administrators" + }, + { + "id": "system-users", + "name": "Users" + }, + { + "id": "system-read-only", + "name": "Read Only" + } + ], + "credentials": [ + { + "id": "9beb9518028c4616948324f9e7e2d071", + "user_id": "7e27048212fd43d78e900bed6f4ac44c", + "auth_provider_type": "homeassistant", + "auth_provider_id": null, + "data": { + "username": "vlb" + } + } + ], + "refresh_tokens": [ + { + "id": "a3f4e8066389495ea030efd0bf0d5cd4", + "user_id": "563d33967d924c588c6ecbff5eb96ed8", + "client_id": null, + "client_name": null, + "client_icon": null, + "token_type": "system", + "created_at": "2022-04-01T07:45:31.978398+00:00", + "access_token_expiration": 1800.0, + "token": "e4059f19c8a74f70691dafb6f8f67c7dddc2f6389020cb6c7d9a5596a0eedee4e523e8ef00c56f2e06ebe8eda4c26a26f12764a9802cf24c876e2f8f09418a50", + "jwt_key": "38697b84f9313b5652b1f8d85022617e8cc7ef3c6299225cc28af8e519694818ef3634c106e53c1f6e5bda25c17079c61a06fd0ba661800a642cd7d9f350c29c", + "last_used_at": null, + "last_used_ip": null, + "credential_id": null, + "version": "2022.3.8" + }, + { + "id": "58d1aab125b742f98856591680c886f8", + "user_id": "7e27048212fd43d78e900bed6f4ac44c", + "client_id": "https://test.tiarkaerell.com/", + "client_name": null, + "client_icon": null, + "token_type": "normal", + "created_at": "2022-04-01T08:04:48.046872+00:00", + "access_token_expiration": 1800.0, + "token": "149461688179de15bb59d25377c890569b12a53cd3607a132d80089cdaa8e4539bc51bfd2b26e93a4574517181c245dfdd8a4b7dbd9fa58b110a834f4ac07306", + "jwt_key": "4516dea1cd209768b3918cdb9023464f08778c3f853511dbf524328abbd70e232c5808fd60c05b59fa500e30cfeef24d5c4fafed357965ecdab9c4a3dbd8123d", + "last_used_at": "2022-04-01T08:04:48.047248+00:00", + "last_used_ip": "85.195.218.245", + "credential_id": "9beb9518028c4616948324f9e7e2d071", + "version": "2022.3.8" + }, + { + "id": "e0c00138ce364f58af0fae84be751b11", + "user_id": "7e27048212fd43d78e900bed6f4ac44c", + "client_id": "https://test.tiarkaerell.com/", + "client_name": null, + "client_icon": null, + "token_type": "normal", + "created_at": "2022-04-01T08:11:29.188371+00:00", + "access_token_expiration": 1800.0, + "token": "f3098610d7491485f26994ffcd37e51b32cd46d5719b1a5beab3063c61ca67d0eeb5c6416f1c77d01a420ab16465491cd3703cb1139018277d48476ac7c623de", + "jwt_key": "b954d0236661f8bffcf6d25568c06ea906e6c141ea644eca94f50a382d5a84defb07526fca7e61558db216eb6d1d5039be8d1eb04db4ce1d36e50f9731745388", + "last_used_at": "2022-04-01T10:28:18.393196+00:00", + "last_used_ip": "85.195.218.245", + "credential_id": "9beb9518028c4616948324f9e7e2d071", + "version": "2022.3.8" + }, + { + "id": "c4fbf4356edf49a888812de23ecc63b6", + "user_id": "7e27048212fd43d78e900bed6f4ac44c", + "client_id": "https://test.tiarkaerell.com/", + "client_name": null, + "client_icon": null, + "token_type": "normal", + "created_at": "2022-04-01T10:46:28.226111+00:00", + "access_token_expiration": 1800.0, + "token": "81f8a38953cc3f3ad6d4a1622baac99d94b1ba939b8388b27a6aff1da417538f67841ae07e23fed18674810b18b9eac1f539dbfae556b15df871a7c4ea7efd0e", + "jwt_key": "5053a085fa4621225e4d53c3e26a934d2c92b418c1f98167e614a572d42d4381c526fae86e21f508014686645eb65a0c68c0821ec6c8e1a47e8fc59fe6fedb32", + "last_used_at": "2022-04-01T10:46:28.227004+00:00", + "last_used_ip": "84.74.113.94", + "credential_id": "9beb9518028c4616948324f9e7e2d071", + "version": "2022.3.8" + } + ] + } +} \ No newline at end of file diff --git a/Config/.storage/auth_provider.homeassistant b/Config/.storage/auth_provider.homeassistant new file mode 100644 index 0000000..41611ee --- /dev/null +++ b/Config/.storage/auth_provider.homeassistant @@ -0,0 +1,13 @@ +{ + "version": 1, + "minor_version": 1, + "key": "auth_provider.homeassistant", + "data": { + "users": [ + { + "username": "vlb", + "password": "JDJiJDEyJEg4Mkg2VHU5aXRCUjZPdmczUDNYQS5vVHplYjJIaUFUVDkyMmIuU1F6QVgvcUs2LnJhaTBt" + } + ] + } +} \ No newline at end of file diff --git a/Config/.storage/core.analytics b/Config/.storage/core.analytics new file mode 100644 index 0000000..fdd6493 --- /dev/null +++ b/Config/.storage/core.analytics @@ -0,0 +1,10 @@ +{ + "version": 1, + "minor_version": 1, + "key": "core.analytics", + "data": { + "preferences": {}, + "onboarded": true, + "uuid": null + } +} \ No newline at end of file diff --git a/Config/.storage/core.area_registry b/Config/.storage/core.area_registry new file mode 100644 index 0000000..67d5122 --- /dev/null +++ b/Config/.storage/core.area_registry @@ -0,0 +1,24 @@ +{ + "version": 1, + "minor_version": 1, + "key": "core.area_registry", + "data": { + "areas": [ + { + "name": "Salon", + "id": "salon", + "picture": null + }, + { + "name": "Cuisine", + "id": "cuisine", + "picture": null + }, + { + "name": "Chambre", + "id": "chambre", + "picture": null + } + ] + } +} \ No newline at end of file diff --git a/Config/.storage/core.config b/Config/.storage/core.config new file mode 100644 index 0000000..4e9d9f6 --- /dev/null +++ b/Config/.storage/core.config @@ -0,0 +1,16 @@ +{ + "version": 1, + "minor_version": 1, + "key": "core.config", + "data": { + "latitude": 52.3731339, + "longitude": 4.8903147, + "elevation": 0, + "unit_system": "metric", + "location_name": "Maison", + "time_zone": "UTC", + "external_url": null, + "internal_url": null, + "currency": "EUR" + } +} \ No newline at end of file diff --git a/Config/.storage/core.config_entries b/Config/.storage/core.config_entries new file mode 100644 index 0000000..24752b5 --- /dev/null +++ b/Config/.storage/core.config_entries @@ -0,0 +1,38 @@ +{ + "version": 1, + "minor_version": 1, + "key": "core.config_entries", + "data": { + "entries": [ + { + "entry_id": "b869fb6b3dbead7e9a32f0712597fc22", + "version": 1, + "domain": "radio_browser", + "title": "Radio Browser", + "data": {}, + "options": {}, + "pref_disable_new_entities": false, + "pref_disable_polling": false, + "source": "onboarding", + "unique_id": null, + "disabled_by": null + }, + { + "entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "version": 1, + "domain": "vesync", + "title": "vlebourl@gmail.com", + "data": { + "username": "vlebourl@gmail.com", + "password": "pVMMJG6NHTxFFC" + }, + "options": {}, + "pref_disable_new_entities": false, + "pref_disable_polling": false, + "source": "user", + "unique_id": "vlebourl@gmail.com-6838532", + "disabled_by": null + } + ] + } +} \ No newline at end of file diff --git a/Config/.storage/core.device_registry b/Config/.storage/core.device_registry new file mode 100644 index 0000000..aabde04 --- /dev/null +++ b/Config/.storage/core.device_registry @@ -0,0 +1,34 @@ +{ + "version": 1, + "minor_version": 3, + "key": "core.device_registry", + "data": { + "devices": [ + { + "config_entries": [ + "0d5d11a7d4cf40ae0800a418ed940bb3" + ], + "connections": [], + "identifiers": [ + [ + "vesync", + "vsaq8556ccd84d7298c822b9bf950366" + ] + ], + "manufacturer": "VeSync", + "model": "LUH-D301S-WEU", + "name": "Humidificateur", + "sw_version": null, + "hw_version": null, + "entry_type": null, + "id": "8fdadc0cedefbc28016545713ccc7f02", + "via_device_id": null, + "area_id": null, + "name_by_user": null, + "disabled_by": null, + "configuration_url": null + } + ], + "deleted_devices": [] + } +} \ No newline at end of file diff --git a/Config/.storage/core.entity_registry b/Config/.storage/core.entity_registry new file mode 100644 index 0000000..88dba3e --- /dev/null +++ b/Config/.storage/core.entity_registry @@ -0,0 +1,264 @@ +{ + "version": 1, + "minor_version": 5, + "key": "core.entity_registry", + "data": { + "entities": [ + { + "area_id": null, + "capabilities": null, + "config_entry_id": null, + "device_class": null, + "device_id": null, + "disabled_by": null, + "entity_category": null, + "entity_id": "person.vlb", + "icon": null, + "id": "cac156ee969a158d3d7f6eddc27a9f13", + "name": null, + "options": {}, + "original_device_class": null, + "original_icon": null, + "original_name": "vlb", + "platform": "person", + "supported_features": 0, + "unique_id": "vlb", + "unit_of_measurement": null + }, + { + "area_id": null, + "capabilities": null, + "config_entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "device_class": null, + "device_id": "8fdadc0cedefbc28016545713ccc7f02", + "disabled_by": null, + "entity_category": "config", + "entity_id": "switch.humidificateur_display", + "icon": null, + "id": "61be826e1b8d0a297882c0479e99e5e5", + "name": null, + "options": {}, + "original_device_class": null, + "original_icon": null, + "original_name": "Humidificateur display", + "platform": "vesync", + "supported_features": 0, + "unique_id": "vsaq8556ccd84d7298c822b9bf950366-display", + "unit_of_measurement": null + }, + { + "area_id": null, + "capabilities": { + "state_class": "measurement" + }, + "config_entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "device_class": null, + "device_id": "8fdadc0cedefbc28016545713ccc7f02", + "disabled_by": null, + "entity_category": "diagnostic", + "entity_id": "sensor.humidificateur_current_humidity", + "icon": null, + "id": "99c7e35a8b96bd6b41db17687b94b693", + "name": null, + "options": {}, + "original_device_class": "humidity", + "original_icon": null, + "original_name": "Humidificateur current humidity", + "platform": "vesync", + "supported_features": 0, + "unique_id": "vsaq8556ccd84d7298c822b9bf950366-humidity", + "unit_of_measurement": "%" + }, + { + "area_id": null, + "capabilities": null, + "config_entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "device_class": null, + "device_id": "8fdadc0cedefbc28016545713ccc7f02", + "disabled_by": null, + "entity_category": "diagnostic", + "entity_id": "binary_sensor.humidificateur_out_of_water", + "icon": null, + "id": "9444be2d2b974b2a8152532d74d88bad", + "name": null, + "options": {}, + "original_device_class": null, + "original_icon": null, + "original_name": "Humidificateur out of water", + "platform": "vesync", + "supported_features": 0, + "unique_id": "vsaq8556ccd84d7298c822b9bf950366-out_of_water", + "unit_of_measurement": null + }, + { + "area_id": null, + "capabilities": { + "min": 1, + "max": 2, + "step": 1.0, + "mode": "auto" + }, + "config_entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "device_class": null, + "device_id": "8fdadc0cedefbc28016545713ccc7f02", + "disabled_by": null, + "entity_category": "config", + "entity_id": "number.humidificateur_mist_level", + "icon": null, + "id": "4eb8ff6f14afee99ced44a973c9aab40", + "name": null, + "options": {}, + "original_device_class": null, + "original_icon": null, + "original_name": "Humidificateur mist level", + "platform": "vesync", + "supported_features": 0, + "unique_id": "vsaq8556ccd84d7298c822b9bf950366-mist-level", + "unit_of_measurement": null + }, + { + "area_id": null, + "capabilities": { + "min_humidity": 30, + "max_humidity": 80, + "available_modes": [ + "auto", + "sleep", + "manual" + ] + }, + "config_entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "device_class": null, + "device_id": "8fdadc0cedefbc28016545713ccc7f02", + "disabled_by": null, + "entity_category": null, + "entity_id": "humidifier.humidificateur", + "icon": null, + "id": "d504532493c5e3835a04739c93a4fd0c", + "name": null, + "options": {}, + "original_device_class": null, + "original_icon": null, + "original_name": "Humidificateur", + "platform": "vesync", + "supported_features": 1, + "unique_id": "vsaq8556ccd84d7298c822b9bf950366", + "unit_of_measurement": null + }, + { + "area_id": null, + "capabilities": null, + "config_entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "device_class": null, + "device_id": "8fdadc0cedefbc28016545713ccc7f02", + "disabled_by": null, + "entity_category": "config", + "entity_id": "switch.humidificateur_automatic_stop", + "icon": null, + "id": "ab61f5de2c2aa2b85b587c4b157298f5", + "name": null, + "options": {}, + "original_device_class": null, + "original_icon": null, + "original_name": "Humidificateur automatic stop", + "platform": "vesync", + "supported_features": 0, + "unique_id": "vsaq8556ccd84d7298c822b9bf950366-automatic-stop", + "unit_of_measurement": null + }, + { + "area_id": null, + "capabilities": null, + "config_entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "device_class": null, + "device_id": "8fdadc0cedefbc28016545713ccc7f02", + "disabled_by": null, + "entity_category": "diagnostic", + "entity_id": "binary_sensor.humidificateur_water_tank_lifted", + "icon": null, + "id": "9d1b97a64b64cb1689a1a01b8df694fe", + "name": null, + "options": {}, + "original_device_class": null, + "original_icon": null, + "original_name": "Humidificateur water tank lifted", + "platform": "vesync", + "supported_features": 0, + "unique_id": "vsaq8556ccd84d7298c822b9bf950366-water_tank_lifted", + "unit_of_measurement": null + }, + { + "area_id": null, + "capabilities": { + "min": 30, + "max": 80, + "step": 1.0, + "mode": "auto" + }, + "config_entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "device_class": null, + "device_id": "8fdadc0cedefbc28016545713ccc7f02", + "disabled_by": null, + "entity_category": "config", + "entity_id": "number.humidificateur_target_level", + "icon": null, + "id": "653aa2a575961a9d2677a83bc584a6ea", + "name": null, + "options": {}, + "original_device_class": null, + "original_icon": null, + "original_name": "Humidificateur target level", + "platform": "vesync", + "supported_features": 0, + "unique_id": "vsaq8556ccd84d7298c822b9bf950366-target-level", + "unit_of_measurement": null + }, + { + "area_id": null, + "capabilities": null, + "config_entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "device_class": null, + "device_id": "8fdadc0cedefbc28016545713ccc7f02", + "disabled_by": null, + "entity_category": "config", + "entity_id": "switch.humidificateur_auto_mode", + "icon": null, + "id": "8fcb923b00a0fe95b1ee0670eb1714aa", + "name": null, + "options": {}, + "original_device_class": null, + "original_icon": null, + "original_name": "Humidificateur auto mode", + "platform": "vesync", + "supported_features": 0, + "unique_id": "vsaq8556ccd84d7298c822b9bf950366-auto-mode", + "unit_of_measurement": null + }, + { + "area_id": null, + "capabilities": { + "supported_color_modes": [ + "brightness" + ] + }, + "config_entry_id": "0d5d11a7d4cf40ae0800a418ed940bb3", + "device_class": null, + "device_id": "8fdadc0cedefbc28016545713ccc7f02", + "disabled_by": null, + "entity_category": "config", + "entity_id": "light.humidificateur_night_light", + "icon": null, + "id": "66bf16efc1261a19c9c99db424275026", + "name": null, + "options": {}, + "original_device_class": null, + "original_icon": null, + "original_name": "Humidificateur night light", + "platform": "vesync", + "supported_features": 0, + "unique_id": "vsaq8556ccd84d7298c822b9bf950366-night-light", + "unit_of_measurement": null + } + ] + } +} \ No newline at end of file diff --git a/Config/.storage/core.restore_state b/Config/.storage/core.restore_state new file mode 100644 index 0000000..1f49890 --- /dev/null +++ b/Config/.storage/core.restore_state @@ -0,0 +1,28 @@ +{ + "version": 1, + "minor_version": 1, + "key": "core.restore_state", + "data": [ + { + "state": { + "entity_id": "person.vlb", + "state": "unknown", + "attributes": { + "editable": false, + "id": "vlb", + "user_id": "7e27048212fd43d78e900bed6f4ac44c", + "friendly_name": "vlb" + }, + "last_changed": "2022-04-01T10:41:26.492401+00:00", + "last_updated": "2022-04-01T10:41:30.405583+00:00", + "context": { + "id": "4916b780086bb9d8250804141e852b6f", + "parent_id": null, + "user_id": null + } + }, + "extra_data": null, + "last_seen": "2022-04-01T10:56:55.367958+00:00" + } + ] +} \ No newline at end of file diff --git a/Config/.storage/core.uuid b/Config/.storage/core.uuid new file mode 100644 index 0000000..3c7ee63 --- /dev/null +++ b/Config/.storage/core.uuid @@ -0,0 +1,8 @@ +{ + "version": 1, + "minor_version": 1, + "key": "core.uuid", + "data": { + "uuid": "3b5ed1182c11445a93dc24d7737bf468" + } +} \ No newline at end of file diff --git a/Config/.storage/http b/Config/.storage/http new file mode 100644 index 0000000..3fb4dc2 --- /dev/null +++ b/Config/.storage/http @@ -0,0 +1,18 @@ +{ + "version": 1, + "minor_version": 1, + "key": "http", + "data": { + "server_port": 9443, + "use_x_forwarded_for": true, + "trusted_proxies": [ + "192.168.1.194" + ], + "ip_ban_enabled": true, + "cors_allowed_origins": [ + "https://cast.home-assistant.io" + ], + "login_attempts_threshold": -1, + "ssl_profile": "modern" + } +} \ No newline at end of file diff --git a/Config/.storage/http.auth b/Config/.storage/http.auth new file mode 100644 index 0000000..637c49c --- /dev/null +++ b/Config/.storage/http.auth @@ -0,0 +1,8 @@ +{ + "version": 1, + "minor_version": 1, + "key": "http.auth", + "data": { + "content_user": "563d33967d924c588c6ecbff5eb96ed8" + } +} \ No newline at end of file diff --git a/Config/.storage/onboarding b/Config/.storage/onboarding new file mode 100644 index 0000000..0b95ba1 --- /dev/null +++ b/Config/.storage/onboarding @@ -0,0 +1,13 @@ +{ + "version": 4, + "minor_version": 1, + "key": "onboarding", + "data": { + "done": [ + "user", + "core_config", + "analytics", + "integration" + ] + } +} \ No newline at end of file diff --git a/Config/.storage/person b/Config/.storage/person new file mode 100644 index 0000000..63ab690 --- /dev/null +++ b/Config/.storage/person @@ -0,0 +1,15 @@ +{ + "version": 2, + "minor_version": 1, + "key": "person", + "data": { + "items": [ + { + "name": "vlb", + "user_id": "7e27048212fd43d78e900bed6f4ac44c", + "device_trackers": [], + "id": "vlb" + } + ] + } +} \ No newline at end of file diff --git a/Config/.storage/trace.saved_traces b/Config/.storage/trace.saved_traces new file mode 100644 index 0000000..77569dc --- /dev/null +++ b/Config/.storage/trace.saved_traces @@ -0,0 +1,6 @@ +{ + "version": 1, + "minor_version": 1, + "key": "trace.saved_traces", + "data": {} +} \ No newline at end of file diff --git a/Config/automations.yaml b/Config/automations.yaml new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/Config/automations.yaml @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/Config/blueprints/automation/homeassistant/motion_light.yaml b/Config/blueprints/automation/homeassistant/motion_light.yaml new file mode 100644 index 0000000..54a4a4f --- /dev/null +++ b/Config/blueprints/automation/homeassistant/motion_light.yaml @@ -0,0 +1,54 @@ +blueprint: + name: Motion-activated Light + description: Turn on a light when motion is detected. + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/motion_light.yaml + input: + motion_entity: + name: Motion Sensor + selector: + entity: + domain: binary_sensor + device_class: motion + light_target: + name: Light + selector: + target: + entity: + domain: light + no_motion_wait: + name: Wait time + description: Time to leave the light on after last motion is detected. + default: 120 + selector: + number: + min: 0 + max: 3600 + unit_of_measurement: seconds + +# If motion is detected within the delay, +# we restart the script. +mode: restart +max_exceeded: silent + +trigger: + platform: state + entity_id: !input motion_entity + from: "off" + to: "on" + +action: + - alias: "Turn on the light" + service: light.turn_on + target: !input light_target + - alias: "Wait until there is no motion from device" + wait_for_trigger: + platform: state + entity_id: !input motion_entity + from: "on" + to: "off" + - alias: "Wait the number of seconds that has been set" + delay: !input no_motion_wait + - alias: "Turn off the light" + service: light.turn_off + target: !input light_target diff --git a/Config/blueprints/automation/homeassistant/notify_leaving_zone.yaml b/Config/blueprints/automation/homeassistant/notify_leaving_zone.yaml new file mode 100644 index 0000000..1dc8a0e --- /dev/null +++ b/Config/blueprints/automation/homeassistant/notify_leaving_zone.yaml @@ -0,0 +1,46 @@ +blueprint: + name: Zone Notification + description: Send a notification to a device when a person leaves a specific zone. + domain: automation + source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/automation/blueprints/notify_leaving_zone.yaml + input: + person_entity: + name: Person + selector: + entity: + domain: person + zone_entity: + name: Zone + selector: + entity: + domain: zone + notify_device: + name: Device to notify + description: Device needs to run the official Home Assistant app to receive notifications. + selector: + device: + integration: mobile_app + +trigger: + platform: state + entity_id: !input person_entity + +variables: + zone_entity: !input zone_entity + # This is the state of the person when it's in this zone. + zone_state: "{{ states[zone_entity].name }}" + person_entity: !input person_entity + person_name: "{{ states[person_entity].name }}" + +condition: + condition: template + # The first case handles leaving the Home zone which has a special state when zoning called 'home'. + # The second case handles leaving all other zones. + value_template: "{{ zone_entity == 'zone.home' and trigger.from_state.state == 'home' and trigger.to_state.state != 'home' or trigger.from_state.state == zone_state and trigger.to_state.state != zone_state }}" + +action: + - alias: "Notify that a person has left the zone" + domain: mobile_app + type: notify + device_id: !input notify_device + message: "{{ person_name }} has left {{ zone_state }}" diff --git a/Config/blueprints/script/homeassistant/confirmable_notification.yaml b/Config/blueprints/script/homeassistant/confirmable_notification.yaml new file mode 100644 index 0000000..d52e5a6 --- /dev/null +++ b/Config/blueprints/script/homeassistant/confirmable_notification.yaml @@ -0,0 +1,84 @@ +blueprint: + name: Confirmable Notification + description: >- + A script that sends an actionable notification with a confirmation before + running the specified action. + domain: script + source_url: https://github.com/home-assistant/core/blob/master/homeassistant/components/script/blueprints/confirmable_notification.yaml + input: + notify_device: + name: Device to notify + description: Device needs to run the official Home Assistant app to receive notifications. + selector: + device: + integration: mobile_app + title: + name: "Title" + description: "The title of the button shown in the notification." + default: "" + selector: + text: + message: + name: "Message" + description: "The message body" + selector: + text: + confirm_text: + name: "Confirmation Text" + description: "Text to show on the confirmation button" + default: "Confirm" + selector: + text: + confirm_action: + name: "Confirmation Action" + description: "Action to run when notification is confirmed" + default: [] + selector: + action: + dismiss_text: + name: "Dismiss Text" + description: "Text to show on the dismiss button" + default: "Dismiss" + selector: + text: + dismiss_action: + name: "Dismiss Action" + description: "Action to run when notification is dismissed" + default: [] + selector: + action: + +mode: restart + +sequence: + - alias: "Set up variables" + variables: + action_confirm: "{{ 'CONFIRM_' ~ context.id }}" + action_dismiss: "{{ 'DISMISS_' ~ context.id }}" + - alias: "Send notification" + domain: mobile_app + type: notify + device_id: !input notify_device + title: !input title + message: !input message + data: + actions: + - action: "{{ action_confirm }}" + title: !input confirm_text + - action: "{{ action_dismiss }}" + title: !input dismiss_text + - alias: "Awaiting response" + wait_for_trigger: + - platform: event + event_type: mobile_app_notification_action + event_data: + action: "{{ action_confirm }}" + - platform: event + event_type: mobile_app_notification_action + event_data: + action: "{{ action_dismiss }}" + - choose: + - conditions: "{{ wait.trigger.event.data.action == action_confirm }}" + sequence: !input confirm_action + - conditions: "{{ wait.trigger.event.data.action == action_dismiss }}" + sequence: !input dismiss_action diff --git a/Config/configuration.yaml b/Config/configuration.yaml new file mode 100644 index 0000000..df1fc1b --- /dev/null +++ b/Config/configuration.yaml @@ -0,0 +1,21 @@ +# Configure a default setup of Home Assistant (frontend, api, etc) +default_config: + +# Text to speech +tts: + - platform: google_translate + +http: + server_port: 9443 + use_x_forwarded_for: true + trusted_proxies: + - 192.168.1.194 + +automation: !include automations.yaml +script: !include scripts.yaml +scene: !include scenes.yaml + +logger: + default: info + logs: + custom_components.vesync: debug diff --git a/Config/custom_components b/Config/custom_components new file mode 120000 index 0000000..4bab8d7 --- /dev/null +++ b/Config/custom_components @@ -0,0 +1 @@ +../custom_components \ No newline at end of file diff --git a/Config/home-assistant.log b/Config/home-assistant.log new file mode 100644 index 0000000..18cb975 --- /dev/null +++ b/Config/home-assistant.log @@ -0,0 +1,443 @@ +2022-04-01 12:41:25 WARNING (SyncWorker_0) [homeassistant.loader] We found a custom integration vesync which has not been tested by Home Assistant. This component might cause stability problems, be sure to disable it if you experience issues with Home Assistant +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain logger took 0.0 seconds +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up system_log +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain system_log took 0.0 seconds +2022-04-01 12:41:25 WARNING (Recorder) [homeassistant.components.recorder.util] The system could not validate that the sqlite3 database at //home/vlb/temp/HAcore/vesync-bpo/Config/home-assistant_v2.db was shutdown cleanly +2022-04-01 12:41:25 WARNING (Recorder) [homeassistant.components.recorder.util] Ended unfinished session (id=16 from 2022-04-01 10:28:05.941295) +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain recorder took 0.1 seconds +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.bootstrap] Setting up stage 1: {'ssdp', 'lovelace', 'webhook', 'http', 'websocket_api', 'device_automation', 'person', 'zeroconf', 'analytics', 'dhcp', 'cloud', 'onboarding', 'api', 'usb', 'diagnostics', 'network', 'search', 'config', 'frontend', 'auth', 'image', 'system_log'} +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up lovelace +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up http +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up device_automation +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain device_automation took 0.0 seconds +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up dhcp +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain dhcp took 0.0 seconds +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain lovelace took 0.1 seconds +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain http took 0.1 seconds +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up webhook +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain webhook took 0.0 seconds +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up websocket_api +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain websocket_api took 0.0 seconds +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up api +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain api took 0.0 seconds +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up diagnostics +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up config +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setting up auth +2022-04-01 12:41:25 INFO (MainThread) [homeassistant.setup] Setup of domain auth took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain diagnostics took 0.1 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain config took 0.1 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up image +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up search +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain search took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up analytics +2022-04-01 12:41:26 INFO (SyncWorker_2) [homeassistant.loader] Loaded google_assistant from homeassistant.components.google_assistant +2022-04-01 12:41:26 INFO (SyncWorker_3) [homeassistant.loader] Loaded alexa from homeassistant.components.alexa +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up usb +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain usb took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up network +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain network took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain image took 0.1 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain analytics took 0.0 seconds +2022-04-01 12:41:26 INFO (SyncWorker_0) [homeassistant.loader] Loaded camera from homeassistant.components.camera +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up person +2022-04-01 12:41:26 INFO (SyncWorker_4) [homeassistant.loader] Loaded media_player from homeassistant.components.media_player +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up zeroconf +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up ssdp +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain ssdp took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain zeroconf took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up cloud +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain cloud took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain person took 0.3 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up onboarding +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain onboarding took 0.0 seconds +2022-04-01 12:41:26 INFO (SyncWorker_0) [homeassistant.loader] Loaded hassio from homeassistant.components.hassio +2022-04-01 12:41:26 INFO (SyncWorker_4) [homeassistant.loader] Loaded device_tracker from homeassistant.components.device_tracker +2022-04-01 12:41:26 INFO (SyncWorker_3) [homeassistant.loader] Loaded group from homeassistant.components.group +2022-04-01 12:41:26 INFO (SyncWorker_2) [homeassistant.loader] Loaded panel_custom from homeassistant.components.panel_custom +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up frontend +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain frontend took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.bootstrap] Setting up stage 2: {'energy', 'input_button', 'input_boolean', 'input_datetime', 'media_source', 'vesync', 'trace', 'history', 'map', 'tts', 'my', 'default_config', 'blueprint', 'input_number', 'counter', 'tag', 'input_select', 'zone', 'logbook', 'scene', 'mobile_app', 'automation', 'timer', 'system_health', 'sun', 'radio_browser', 'script', 'input_text'} +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up input_button +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up input_boolean +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up input_datetime +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up media_source +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up vesync +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain vesync took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up trace +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain trace took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up history +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain history took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up map +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain map took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up my +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain my took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up blueprint +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain blueprint took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up input_number +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up counter +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up tag +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up input_select +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up zone +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up logbook +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up scene +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.components.scene] Setting up scene.homeassistant +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up timer +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up system_health +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up sun +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain sun took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up radio_browser +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain radio_browser took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up input_text +2022-04-01 12:41:26 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v1/user/login' api +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain media_source took 0.2 seconds +2022-04-01 12:41:26 INFO (SyncWorker_0) [homeassistant.loader] Loaded tado from homeassistant.components.tado +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain logbook took 0.1 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain scene took 0.1 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up automation +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain system_health took 0.1 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up script +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up energy +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain energy took 0.0 seconds +2022-04-01 12:41:26 INFO (SyncWorker_1) [homeassistant.loader] Loaded google_translate from homeassistant.components.google_translate +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain automation took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain script took 0.0 seconds +2022-04-01 12:41:26 INFO (SyncWorker_5) [homeassistant.loader] Loaded sensor from homeassistant.components.sensor +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up tts +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain input_button took 0.2 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain tag took 0.2 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain input_datetime took 0.2 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain input_boolean took 0.2 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain input_number took 0.2 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.components.http] Now listening on port 9443 +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setting up sensor +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain sensor took 0.0 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain counter took 0.2 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain input_select took 0.2 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.components.sensor] Setting up sensor.energy +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain zone took 0.2 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain timer took 0.2 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain input_text took 0.2 seconds +2022-04-01 12:41:26 INFO (MainThread) [homeassistant.setup] Setup of domain tts took 0.1 seconds +2022-04-01 12:41:26 INFO (SyncWorker_5) [homeassistant.loader] Loaded notify from homeassistant.components.notify +2022-04-01 12:41:27 INFO (MainThread) [homeassistant.setup] Setting up mobile_app +2022-04-01 12:41:27 INFO (MainThread) [homeassistant.setup] Setup of domain mobile_app took 0.0 seconds +2022-04-01 12:41:27 INFO (MainThread) [homeassistant.setup] Setting up notify +2022-04-01 12:41:27 INFO (MainThread) [homeassistant.setup] Setup of domain notify took 0.0 seconds +2022-04-01 12:41:27 INFO (MainThread) [homeassistant.setup] Setting up default_config +2022-04-01 12:41:27 INFO (MainThread) [homeassistant.setup] Setup of domain default_config took 0.0 seconds +2022-04-01 12:41:27 INFO (MainThread) [homeassistant.components.notify] Setting up notify.mobile_app +2022-04-01 12:41:27 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v1/deviceManaged/devices' api +2022-04-01 12:41:27 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.vesync] New device list initialized +2022-04-01 12:41:27 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:28 INFO (MainThread) [custom_components.vesync.common] 1 VeSync fans found +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.components.sensor] Setting up sensor.vesync +2022-04-01 12:41:28 INFO (SyncWorker_1) [homeassistant.loader] Loaded switch from homeassistant.components.switch +2022-04-01 12:41:28 INFO (SyncWorker_5) [homeassistant.loader] Loaded fan from homeassistant.components.fan +2022-04-01 12:41:28 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:28 INFO (SyncWorker_3) [homeassistant.loader] Loaded light from homeassistant.components.light +2022-04-01 12:41:28 INFO (SyncWorker_2) [homeassistant.loader] Loaded number from homeassistant.components.number +2022-04-01 12:41:28 INFO (SyncWorker_1) [homeassistant.loader] Loaded binary_sensor from homeassistant.components.binary_sensor +2022-04-01 12:41:28 INFO (SyncWorker_0) [homeassistant.loader] Loaded humidifier from homeassistant.components.humidifier +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setting up switch +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setup of domain switch took 0.0 seconds +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setting up fan +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setup of domain fan took 0.0 seconds +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setting up light +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setting up number +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setup of domain number took 0.0 seconds +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setting up binary_sensor +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setup of domain binary_sensor took 0.0 seconds +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setting up humidifier +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setup of domain humidifier took 0.0 seconds +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.components.switch] Setting up switch.vesync +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.components.fan] Setting up fan.vesync +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.setup] Setup of domain light took 0.0 seconds +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.components.number] Setting up number.vesync +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.components.binary_sensor] Setting up binary_sensor.vesync +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.components.humidifier] Setting up humidifier.vesync +2022-04-01 12:41:28 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:28 INFO (MainThread) [homeassistant.components.light] Setting up light.vesync +2022-04-01 12:41:28 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:28 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:28 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:29 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:29 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:29 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:29 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:30 INFO (MainThread) [homeassistant.bootstrap] Home Assistant initialized in 5.01s +2022-04-01 12:41:30 INFO (MainThread) [homeassistant.core] Starting Home Assistant +2022-04-01 12:41:30 INFO (MainThread) [homeassistant.core] Timer:starting +2022-04-01 12:41:30 INFO (MainThread) [homeassistant.components.zeroconf] Starting Zeroconf broadcast +2022-04-01 12:41:30 INFO (SyncWorker_3) [homeassistant.loader] Loaded overkiz from homeassistant.components.overkiz +2022-04-01 12:41:30 INFO (SyncWorker_1) [homeassistant.loader] Loaded hue from homeassistant.components.hue +2022-04-01 12:41:31 INFO (SyncWorker_4) [homeassistant.loader] Loaded somfy from homeassistant.components.somfy +2022-04-01 12:41:31 INFO (SyncWorker_4) [homeassistant.loader] Loaded upnp from homeassistant.components.upnp +2022-04-01 12:41:31 INFO (SyncWorker_5) [homeassistant.loader] Loaded cast from homeassistant.components.cast +2022-04-01 12:41:31 INFO (SyncWorker_1) [homeassistant.loader] Loaded plex from homeassistant.components.plex +2022-04-01 12:41:31 INFO (SyncWorker_0) [homeassistant.loader] Loaded homekit_controller from homeassistant.components.homekit_controller +2022-04-01 12:41:32 INFO (SyncWorker_3) [homeassistant.loader] Loaded netatmo from homeassistant.components.netatmo +2022-04-01 12:41:32 INFO (SyncWorker_2) [homeassistant.loader] Loaded ipp from homeassistant.components.ipp +2022-04-01 12:41:32 INFO (SyncWorker_4) [homeassistant.loader] Loaded esphome from homeassistant.components.esphome +2022-04-01 12:41:32 INFO (SyncWorker_5) [homeassistant.loader] Loaded harmony from homeassistant.components.harmony +2022-04-01 12:41:32 INFO (SyncWorker_1) [homeassistant.loader] Loaded remote from homeassistant.components.remote +2022-04-01 12:41:32 INFO (MainThread) [homeassistant.setup] Setting up remote +2022-04-01 12:41:32 INFO (MainThread) [homeassistant.setup] Setup of domain remote took 0.0 seconds +2022-04-01 12:41:32 INFO (SyncWorker_3) [homeassistant.loader] Loaded dlna_dms from homeassistant.components.dlna_dms +2022-04-01 12:41:32 INFO (SyncWorker_2) [homeassistant.loader] Loaded devolo_home_control from homeassistant.components.devolo_home_control +2022-04-01 12:41:32 INFO (SyncWorker_4) [homeassistant.loader] Loaded devolo_home_network from homeassistant.components.devolo_home_network +2022-04-01 12:41:59 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:59 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:00 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:00 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:00 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:01 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:29 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:29 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:29 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:30 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:30 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:30 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:31 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:31 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:59 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:42:59 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:00 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:00 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:01 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:01 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:29 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:29 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:29 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:30 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:30 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:30 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:31 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:31 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:59 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:43:59 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:00 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:00 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:00 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:01 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:01 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:29 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:29 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:29 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:30 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:30 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:30 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:31 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:31 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:59 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:44:59 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:00 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:00 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:00 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:01 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:29 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:29 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:29 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:30 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:30 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:30 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:30 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:59 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:45:59 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:00 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:00 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:00 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:01 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:01 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:29 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:29 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:30 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:30 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:30 DEBUG (SyncWorker_9) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:30 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:30 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:31 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:31 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:46:59 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:00 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:00 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:00 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:00 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:00 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:01 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:02 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:29 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:29 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:30 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:30 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:30 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:30 DEBUG (SyncWorker_9) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:31 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:47:59 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:00 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:00 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:00 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:00 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:01 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:29 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:29 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:30 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:30 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:30 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:30 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:31 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:48:59 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:00 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:00 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:00 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:00 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:01 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:01 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:29 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:29 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:30 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:30 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:30 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:30 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:31 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:49:59 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:00 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:00 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:00 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:01 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:01 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:29 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:29 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:30 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:30 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:30 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:30 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:30 DEBUG (SyncWorker_9) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:31 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:50:59 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:00 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:00 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:00 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:00 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:01 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:29 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:29 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:30 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:30 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:30 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:30 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:31 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:51:59 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:00 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:00 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:00 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:01 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:01 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:01 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:29 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:29 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:30 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:30 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:30 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:30 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:31 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:52:59 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:00 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:00 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:00 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:00 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:01 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:01 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:29 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:29 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:30 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:30 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:30 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:30 DEBUG (SyncWorker_9) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:31 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:53:59 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:00 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:00 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:00 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:01 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:01 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:29 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:29 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:30 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:30 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:30 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:30 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:30 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:31 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:54:59 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:00 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:00 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:00 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:01 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:01 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:29 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:29 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:30 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:30 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:30 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:30 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:30 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:31 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:55:59 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:00 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:00 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:00 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:00 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:00 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:01 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:01 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:29 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:29 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:30 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:30 DEBUG (SyncWorker_7) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:30 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:30 DEBUG (SyncWorker_9) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:30 DEBUG (SyncWorker_10) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:31 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:31 DEBUG (SyncWorker_8) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:56:55 INFO (Recorder) [homeassistant.components.recorder.pool.RecorderPool] Pool recreating diff --git a/Config/home-assistant.log.1 b/Config/home-assistant.log.1 new file mode 100644 index 0000000..68a8883 --- /dev/null +++ b/Config/home-assistant.log.1 @@ -0,0 +1,457 @@ +2022-04-01 12:28:05 WARNING (SyncWorker_0) [homeassistant.loader] We found a custom integration vesync which has not been tested by Home Assistant. This component might cause stability problems, be sure to disable it if you experience issues with Home Assistant +2022-04-01 12:28:05 INFO (MainThread) [homeassistant.setup] Setup of domain logger took 0.0 seconds +2022-04-01 12:28:05 INFO (MainThread) [homeassistant.setup] Setting up recorder +2022-04-01 12:28:05 INFO (MainThread) [homeassistant.setup] Setting up system_log +2022-04-01 12:28:05 INFO (MainThread) [homeassistant.setup] Setup of domain system_log took 0.0 seconds +2022-04-01 12:28:05 WARNING (Recorder) [homeassistant.components.recorder.util] The system could not validate that the sqlite3 database at //home/vlb/temp/HAcore/vesync-bpo/Config/home-assistant_v2.db was shutdown cleanly +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain recorder took 0.1 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.bootstrap] Setting up stage 1: {'lovelace', 'analytics', 'diagnostics', 'image', 'api', 'http', 'websocket_api', 'search', 'config', 'system_log', 'device_automation', 'zeroconf', 'frontend', 'auth', 'dhcp', 'onboarding', 'person', 'webhook', 'ssdp', 'network', 'usb', 'cloud'} +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up lovelace +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up http +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up device_automation +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain device_automation took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up dhcp +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain dhcp took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain lovelace took 0.1 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain http took 0.1 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up diagnostics +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up api +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain api took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up websocket_api +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain websocket_api took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up config +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up auth +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain auth took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up webhook +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain webhook took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain diagnostics took 0.1 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up image +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain config took 0.1 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up analytics +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up search +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain search took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain image took 0.1 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up network +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain network took 0.0 seconds +2022-04-01 12:28:06 INFO (SyncWorker_4) [homeassistant.loader] Loaded google_assistant from homeassistant.components.google_assistant +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up person +2022-04-01 12:28:06 INFO (SyncWorker_1) [homeassistant.loader] Loaded alexa from homeassistant.components.alexa +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up usb +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain usb took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain analytics took 0.0 seconds +2022-04-01 12:28:06 INFO (SyncWorker_0) [homeassistant.loader] Loaded camera from homeassistant.components.camera +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up zeroconf +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up ssdp +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain ssdp took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain zeroconf took 0.0 seconds +2022-04-01 12:28:06 INFO (SyncWorker_4) [homeassistant.loader] Loaded media_player from homeassistant.components.media_player +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up cloud +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain cloud took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain person took 0.3 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up onboarding +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain onboarding took 0.0 seconds +2022-04-01 12:28:06 INFO (SyncWorker_0) [homeassistant.loader] Loaded hassio from homeassistant.components.hassio +2022-04-01 12:28:06 INFO (SyncWorker_3) [homeassistant.loader] Loaded device_tracker from homeassistant.components.device_tracker +2022-04-01 12:28:06 INFO (SyncWorker_1) [homeassistant.loader] Loaded tado from homeassistant.components.tado +2022-04-01 12:28:06 INFO (SyncWorker_2) [homeassistant.loader] Loaded group from homeassistant.components.group +2022-04-01 12:28:06 INFO (SyncWorker_0) [homeassistant.loader] Loaded panel_custom from homeassistant.components.panel_custom +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up frontend +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain frontend took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.bootstrap] Setting up stage 2: {'logbook', 'input_datetime', 'counter', 'zone', 'script', 'history', 'my', 'mobile_app', 'default_config', 'timer', 'tag', 'media_source', 'input_boolean', 'input_button', 'blueprint', 'system_health', 'automation', 'tts', 'energy', 'vesync', 'map', 'scene', 'input_text', 'radio_browser', 'sun', 'trace', 'input_number', 'input_select'} +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up logbook +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up input_datetime +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up counter +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up zone +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up history +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain history took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up my +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain my took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up timer +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up tag +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up media_source +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up input_boolean +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up input_button +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up blueprint +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain blueprint took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up system_health +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up vesync +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain vesync took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up map +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain map took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up scene +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.components.scene] Setting up scene.homeassistant +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up input_text +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up radio_browser +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain radio_browser took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up sun +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain sun took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up trace +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain trace took 0.0 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up input_number +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setting up input_select +2022-04-01 12:28:06 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v1/user/login' api +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain logbook took 0.2 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain media_source took 0.2 seconds +2022-04-01 12:28:06 INFO (MainThread) [homeassistant.setup] Setup of domain system_health took 0.2 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setting up energy +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain energy took 0.0 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain scene took 0.2 seconds +2022-04-01 12:28:07 INFO (SyncWorker_0) [homeassistant.loader] Loaded google_translate from homeassistant.components.google_translate +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setting up script +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setting up automation +2022-04-01 12:28:07 INFO (SyncWorker_3) [homeassistant.loader] Loaded sensor from homeassistant.components.sensor +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setting up tts +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain script took 0.0 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain automation took 0.0 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain input_datetime took 0.3 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain counter took 0.3 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain tag took 0.3 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain timer took 0.3 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setting up sensor +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain sensor took 0.0 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.components.http] Now listening on port 9443 +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain zone took 0.3 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain input_boolean took 0.3 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain input_button took 0.3 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain input_text took 0.3 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.components.sensor] Setting up sensor.energy +2022-04-01 12:28:07 INFO (SyncWorker_0) [homeassistant.loader] Loaded notify from homeassistant.components.notify +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain input_number took 0.2 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain input_select took 0.2 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setting up mobile_app +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain tts took 0.1 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain mobile_app took 0.1 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setting up notify +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain notify took 0.0 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setting up default_config +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.setup] Setup of domain default_config took 0.0 seconds +2022-04-01 12:28:07 INFO (MainThread) [homeassistant.components.notify] Setting up notify.mobile_app +2022-04-01 12:28:07 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v1/deviceManaged/devices' api +2022-04-01 12:28:07 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.vesync] New device list initialized +2022-04-01 12:28:07 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:08 INFO (MainThread) [custom_components.vesync.common] 1 VeSync fans found +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.components.sensor] Setting up sensor.vesync +2022-04-01 12:28:08 INFO (SyncWorker_1) [homeassistant.loader] Loaded humidifier from homeassistant.components.humidifier +2022-04-01 12:28:08 INFO (SyncWorker_5) [homeassistant.loader] Loaded fan from homeassistant.components.fan +2022-04-01 12:28:08 INFO (SyncWorker_4) [homeassistant.loader] Loaded light from homeassistant.components.light +2022-04-01 12:28:08 INFO (SyncWorker_3) [homeassistant.loader] Loaded switch from homeassistant.components.switch +2022-04-01 12:28:08 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:08 INFO (SyncWorker_0) [homeassistant.loader] Loaded number from homeassistant.components.number +2022-04-01 12:28:08 INFO (SyncWorker_2) [homeassistant.loader] Loaded binary_sensor from homeassistant.components.binary_sensor +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setting up humidifier +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setup of domain humidifier took 0.0 seconds +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setting up fan +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setup of domain fan took 0.0 seconds +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setting up light +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setting up switch +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setup of domain switch took 0.0 seconds +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setting up number +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setup of domain number took 0.0 seconds +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setting up binary_sensor +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setup of domain binary_sensor took 0.0 seconds +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.components.humidifier] Setting up humidifier.vesync +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.components.fan] Setting up fan.vesync +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.components.switch] Setting up switch.vesync +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.setup] Setup of domain light took 0.0 seconds +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.components.number] Setting up number.vesync +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.components.binary_sensor] Setting up binary_sensor.vesync +2022-04-01 12:28:08 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:08 INFO (MainThread) [homeassistant.components.light] Setting up light.vesync +2022-04-01 12:28:08 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:08 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:08 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:09 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:09 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:09 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:10 INFO (MainThread) [homeassistant.bootstrap] Home Assistant initialized in 5.20s +2022-04-01 12:28:10 INFO (MainThread) [homeassistant.core] Starting Home Assistant +2022-04-01 12:28:10 INFO (MainThread) [homeassistant.core] Timer:starting +2022-04-01 12:28:10 INFO (MainThread) [homeassistant.components.zeroconf] Starting Zeroconf broadcast +2022-04-01 12:28:10 INFO (SyncWorker_5) [homeassistant.loader] Loaded somfy from homeassistant.components.somfy +2022-04-01 12:28:10 INFO (SyncWorker_0) [homeassistant.loader] Loaded overkiz from homeassistant.components.overkiz +2022-04-01 12:28:11 INFO (SyncWorker_5) [homeassistant.loader] Loaded homekit_controller from homeassistant.components.homekit_controller +2022-04-01 12:28:11 INFO (SyncWorker_2) [homeassistant.loader] Loaded cast from homeassistant.components.cast +2022-04-01 12:28:11 INFO (SyncWorker_3) [homeassistant.loader] Loaded plex from homeassistant.components.plex +2022-04-01 12:28:11 INFO (SyncWorker_3) [homeassistant.loader] Loaded hue from homeassistant.components.hue +2022-04-01 12:28:12 INFO (SyncWorker_6) [homeassistant.loader] Loaded netatmo from homeassistant.components.netatmo +2022-04-01 12:28:12 INFO (SyncWorker_2) [homeassistant.loader] Loaded ipp from homeassistant.components.ipp +2022-04-01 12:28:12 INFO (SyncWorker_3) [homeassistant.loader] Loaded harmony from homeassistant.components.harmony +2022-04-01 12:28:12 INFO (SyncWorker_6) [homeassistant.loader] Loaded remote from homeassistant.components.remote +2022-04-01 12:28:12 INFO (MainThread) [homeassistant.setup] Setting up remote +2022-04-01 12:28:12 INFO (MainThread) [homeassistant.setup] Setup of domain remote took 0.0 seconds +2022-04-01 12:28:12 INFO (SyncWorker_0) [homeassistant.loader] Loaded upnp from homeassistant.components.upnp +2022-04-01 12:28:13 INFO (SyncWorker_2) [homeassistant.loader] Loaded dlna_dms from homeassistant.components.dlna_dms +2022-04-01 12:28:13 INFO (SyncWorker_4) [homeassistant.loader] Loaded devolo_home_network from homeassistant.components.devolo_home_network +2022-04-01 12:28:13 INFO (SyncWorker_3) [homeassistant.loader] Loaded devolo_home_control from homeassistant.components.devolo_home_control +2022-04-01 12:28:13 INFO (SyncWorker_5) [homeassistant.loader] Loaded esphome from homeassistant.components.esphome +2022-04-01 12:28:36 INFO (MainThread) [homeassistant.helpers.script.websocket_api_script] websocket_api script: Running websocket_api script +2022-04-01 12:28:36 INFO (MainThread) [homeassistant.helpers.script.websocket_api_script] websocket_api script: Executing step call service +2022-04-01 12:28:36 INFO (MainThread) [custom_components.vesync.common] 1 VeSync fans found +2022-04-01 12:28:36 WARNING (MainThread) [custom_components.vesync.fan] Humidificateur - Unknown device type - LUH-D301S-WEU +2022-04-01 12:28:36 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:36 INFO (MainThread) [homeassistant.helpers.entity_registry] Registered new light.vesync entity: light.humidificateur_night_light +2022-04-01 12:28:36 ERROR (MainThread) [homeassistant.components.light] Error adding entities for domain light with platform vesync +Traceback (most recent call last): + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity_platform.py", line 382, in async_add_entities + await asyncio.gather(*tasks) + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity_platform.py", line 614, in _async_add_entity + await entity.add_to_platform_finish() + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity.py", line 801, in add_to_platform_finish + self.async_write_ha_state() + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity.py", line 553, in async_write_ha_state + self._async_write_ha_state() + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity.py", line 590, in _async_write_ha_state + state = self._stringify_state() + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity.py", line 559, in _stringify_state + if (state := self.state) is None: + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity.py", line 988, in state + if (is_on := self.is_on) is None: + File "/home/vlb/temp/HAcore/vesync-bpo/Config/custom_components/vesync/light.py", line 234, in is_on + return self.smarthumidifier.details["night_light_brightness"] > 0 +KeyError: 'night_light_brightness' +2022-04-01 12:28:36 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved +Traceback (most recent call last): + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity_platform.py", line 382, in async_add_entities + await asyncio.gather(*tasks) + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity_platform.py", line 614, in _async_add_entity + await entity.add_to_platform_finish() + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity.py", line 801, in add_to_platform_finish + self.async_write_ha_state() + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity.py", line 553, in async_write_ha_state + self._async_write_ha_state() + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity.py", line 590, in _async_write_ha_state + state = self._stringify_state() + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity.py", line 559, in _stringify_state + if (state := self.state) is None: + File "/home/vlb/temp/HAcore/vesync-bpo/.venv/lib/python3.9/site-packages/homeassistant/helpers/entity.py", line 988, in state + if (is_on := self.is_on) is None: + File "/home/vlb/temp/HAcore/vesync-bpo/Config/custom_components/vesync/light.py", line 234, in is_on + return self.smarthumidifier.details["night_light_brightness"] > 0 +KeyError: 'night_light_brightness' +2022-04-01 12:28:39 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:40 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:40 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:40 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:40 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:41 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:28:42 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:07 INFO (MainThread) [homeassistant.helpers.script.websocket_api_script] websocket_api script: Running websocket_api script +2022-04-01 12:29:07 INFO (MainThread) [homeassistant.helpers.script.websocket_api_script] websocket_api script: Executing step call service +2022-04-01 12:29:07 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v1/deviceManaged/devices' api +2022-04-01 12:29:08 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:09 INFO (MainThread) [custom_components.vesync.common] 1 VeSync fans found +2022-04-01 12:29:09 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:09 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:10 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:10 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:10 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:11 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:12 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:39 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:40 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:40 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:40 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:41 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:29:42 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:09 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:09 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:10 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:10 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:10 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:10 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:10 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:11 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:12 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:39 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:40 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:40 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:40 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:40 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:41 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:30:42 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:09 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:09 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:10 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:10 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:10 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:11 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:12 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:39 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:40 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:40 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:40 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:41 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:31:42 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:09 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:09 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:10 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:10 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:10 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:11 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:12 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:39 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:40 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:40 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:40 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:41 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:32:41 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:09 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:09 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:10 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:10 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:10 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:10 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:11 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:12 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:39 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:40 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:40 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:40 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:41 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:33:42 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:09 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:09 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:10 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:10 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:10 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:10 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:11 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:11 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:39 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:40 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:40 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:40 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:41 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:34:42 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:09 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:09 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:10 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:10 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:10 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:11 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:12 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:39 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:40 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:40 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:40 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:41 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:35:41 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:09 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:09 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:10 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:10 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:10 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:11 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:12 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:39 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:40 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:40 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:40 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:41 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:36:42 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:09 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:09 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:10 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:10 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:10 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:11 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:12 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:39 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:40 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:40 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:40 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:41 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:37:42 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:09 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:09 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:10 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:10 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:10 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:11 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:11 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:39 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:40 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:40 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:41 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:41 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:38:41 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:09 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:09 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:10 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:10 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:10 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:11 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:11 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:12 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:39 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:40 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:40 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:41 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:41 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:39:42 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:09 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:09 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:10 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:10 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:11 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:11 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:12 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:39 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:40 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:40 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:40 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:40 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:40 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:41 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:40:41 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:09 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:09 DEBUG (SyncWorker_2) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:10 DEBUG (SyncWorker_0) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:10 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:10 DEBUG (SyncWorker_6) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:10 DEBUG (SyncWorker_4) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:10 DEBUG (SyncWorker_3) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:11 DEBUG (SyncWorker_1) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api +2022-04-01 12:41:11 DEBUG (SyncWorker_5) [custom_components.vesync.pyvesync.helpers] [post] calling '/cloud/v2/deviceManaged/bypassV2' api diff --git a/Config/home-assistant_v2.db b/Config/home-assistant_v2.db new file mode 100644 index 0000000..988257e Binary files /dev/null and b/Config/home-assistant_v2.db differ diff --git a/Config/scenes.yaml b/Config/scenes.yaml new file mode 100644 index 0000000..e69de29 diff --git a/Config/scripts.yaml b/Config/scripts.yaml new file mode 100644 index 0000000..e69de29 diff --git a/Config/secrets.yaml b/Config/secrets.yaml new file mode 100644 index 0000000..c5b900c --- /dev/null +++ b/Config/secrets.yaml @@ -0,0 +1,4 @@ + +# Use this file to store secrets like usernames and passwords. +# Learn more at https://www.home-assistant.io/docs/configuration/secrets/ +some_password: welcome diff --git a/README.md b/README.md new file mode 100644 index 0000000..6030962 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +[![hacs_badge](https://img.shields.io/badge/HACS-Default-orange.svg)](https://github.com/custom-components/hacs) +[![GitHub release](https://img.shields.io/github/release/iMicknl/ha-tahoma.svg)](https://GitHub.com/vlebourl/vesync-bpo/releases/) + +# VeSync custom component for Home Assistant + +Custom component for Home Assistant to interact with smart devices via the VeSync platform. + +## Installation + +You can install this integration via [HACS](#hacs) or [manually](#manual). +This integration will override the core VeSync integration. + +### HACS + +This integration can be installed by adding this repository to HACS, then searching for `VeSync` and choosing install. Reboot Home Assistant and configure the 'VeSync' integration via the integrations page or press the blue button below. + +[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=vesync) + +### Manual + +Copy the `custom_components/vesync` to your `custom_components` folder. Reboot Home Assistant and configure the 'Overkiz (by Somfy)' integration via the integrations page or press the blue button below. + +[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=vesync) + +### Enable debug logging + +The [logger](https://www.home-assistant.io/integrations/logger/) integration lets you define the level of logging activities in Home Assistant. Turning on debug mode will show more information about unsupported devices in your logbook. + +```yaml +logger: + default: error + logs: + custom_components.vesync: debug +``` + +This integration is heavily based on [VeSync_bpo](https://github.com/borpin/vesync-bpo) and [pyvesync](https://pypi.org/project/pyvesync/) diff --git a/custom_components/vesync/__init__.py b/custom_components/vesync/__init__.py new file mode 100644 index 0000000..46689f1 --- /dev/null +++ b/custom_components/vesync/__init__.py @@ -0,0 +1,106 @@ +"""VeSync integration.""" +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .common import async_process_devices +from .const import ( + DOMAIN, + SERVICE_UPDATE_DEVS, + VS_BINARY_SENSORS, + VS_DISCOVERY, + VS_FANS, + VS_HUMIDIFIERS, + VS_LIGHTS, + VS_MANAGER, + VS_NUMBERS, + VS_SENSORS, + VS_SWITCHES, +) +from .pyvesync.vesync import VeSync + +PLATFORMS = { + Platform.SWITCH: VS_SWITCHES, + Platform.FAN: VS_FANS, + Platform.LIGHT: VS_LIGHTS, + Platform.SENSOR: VS_SENSORS, + Platform.HUMIDIFIER: VS_HUMIDIFIERS, + Platform.NUMBER: VS_NUMBERS, + Platform.BINARY_SENSOR: VS_BINARY_SENSORS, +} + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Set up Vesync as config entry.""" + username = config_entry.data[CONF_USERNAME] + password = config_entry.data[CONF_PASSWORD] + + time_zone = str(hass.config.time_zone) + + manager = VeSync(username, password, time_zone) + + login = await hass.async_add_executor_job(manager.login) + + if not login: + _LOGGER.error("Unable to login to the VeSync server") + return False + + device_dict = await async_process_devices(hass, manager) + + forward_setup = hass.config_entries.async_forward_entry_setup + + hass.data[DOMAIN] = {config_entry.entry_id: {}} + hass.data[DOMAIN][config_entry.entry_id][VS_MANAGER] = manager + + for p, vs_p in PLATFORMS.items(): + hass.data[DOMAIN][config_entry.entry_id][vs_p] = [] + if device_dict[VS_SWITCHES]: + hass.data[DOMAIN][config_entry.entry_id][vs_p].extend(device_dict[vs_p]) + hass.async_create_task(forward_setup(config_entry, p)) + + async def async_new_device_discovery(service: ServiceCall) -> None: + """Discover if new devices should be added.""" + manager = hass.data[DOMAIN][config_entry.entry_id][VS_MANAGER] + dev_dict = await async_process_devices(hass, manager) + + def _add_new_devices(platform: str) -> None: + """Add new devices to hass.""" + old_devices = hass.data[DOMAIN][config_entry.entry_id][PLATFORMS[platform]] + if new_devices := list( + set(dev_dict.get(VS_SWITCHES, [])).difference(old_devices) + ): + old_devices.extend(new_devices) + if old_devices: + async_dispatcher_send( + hass, VS_DISCOVERY.format(PLATFORMS[platform]), new_devices + ) + else: + hass.async_create_task(forward_setup(config_entry, platform)) + + for k, v in PLATFORMS.items(): + _add_new_devices(k) + + hass.services.async_register( + DOMAIN, SERVICE_UPDATE_DEVS, async_new_device_discovery + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms( + entry, list(PLATFORMS.keys()) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/custom_components/vesync/__pycache__/__init__.cpython-39.pyc b/custom_components/vesync/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..5d13525 Binary files /dev/null and b/custom_components/vesync/__pycache__/__init__.cpython-39.pyc differ diff --git a/custom_components/vesync/__pycache__/binary_sensor.cpython-39.pyc b/custom_components/vesync/__pycache__/binary_sensor.cpython-39.pyc new file mode 100644 index 0000000..1080623 Binary files /dev/null and b/custom_components/vesync/__pycache__/binary_sensor.cpython-39.pyc differ diff --git a/custom_components/vesync/__pycache__/common.cpython-39.pyc b/custom_components/vesync/__pycache__/common.cpython-39.pyc new file mode 100644 index 0000000..39df93c Binary files /dev/null and b/custom_components/vesync/__pycache__/common.cpython-39.pyc differ diff --git a/custom_components/vesync/__pycache__/config_flow.cpython-39.pyc b/custom_components/vesync/__pycache__/config_flow.cpython-39.pyc new file mode 100644 index 0000000..abc180c Binary files /dev/null and b/custom_components/vesync/__pycache__/config_flow.cpython-39.pyc differ diff --git a/custom_components/vesync/__pycache__/const.cpython-39.pyc b/custom_components/vesync/__pycache__/const.cpython-39.pyc new file mode 100644 index 0000000..8e14c4c Binary files /dev/null and b/custom_components/vesync/__pycache__/const.cpython-39.pyc differ diff --git a/custom_components/vesync/__pycache__/fan.cpython-39.pyc b/custom_components/vesync/__pycache__/fan.cpython-39.pyc new file mode 100644 index 0000000..7119fc2 Binary files /dev/null and b/custom_components/vesync/__pycache__/fan.cpython-39.pyc differ diff --git a/custom_components/vesync/__pycache__/humidifier.cpython-39.pyc b/custom_components/vesync/__pycache__/humidifier.cpython-39.pyc new file mode 100644 index 0000000..3f38984 Binary files /dev/null and b/custom_components/vesync/__pycache__/humidifier.cpython-39.pyc differ diff --git a/custom_components/vesync/__pycache__/light.cpython-39.pyc b/custom_components/vesync/__pycache__/light.cpython-39.pyc new file mode 100644 index 0000000..bbf901d Binary files /dev/null and b/custom_components/vesync/__pycache__/light.cpython-39.pyc differ diff --git a/custom_components/vesync/__pycache__/number.cpython-39.pyc b/custom_components/vesync/__pycache__/number.cpython-39.pyc new file mode 100644 index 0000000..b3c291e Binary files /dev/null and b/custom_components/vesync/__pycache__/number.cpython-39.pyc differ diff --git a/custom_components/vesync/__pycache__/sensor.cpython-39.pyc b/custom_components/vesync/__pycache__/sensor.cpython-39.pyc new file mode 100644 index 0000000..ee5dfa9 Binary files /dev/null and b/custom_components/vesync/__pycache__/sensor.cpython-39.pyc differ diff --git a/custom_components/vesync/__pycache__/switch.cpython-39.pyc b/custom_components/vesync/__pycache__/switch.cpython-39.pyc new file mode 100644 index 0000000..310ee24 Binary files /dev/null and b/custom_components/vesync/__pycache__/switch.cpython-39.pyc differ diff --git a/custom_components/vesync/binary_sensor.py b/custom_components/vesync/binary_sensor.py new file mode 100644 index 0000000..9dcdc91 --- /dev/null +++ b/custom_components/vesync/binary_sensor.py @@ -0,0 +1,103 @@ +"""Support for power & energy sensors for VeSync outlets.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .common import VeSyncBaseEntity, is_humidifier +from .const import DOMAIN, VS_BINARY_SENSORS, VS_DISCOVERY + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up binary sensors.""" + + @callback + def discover(devices): + """Add new devices to platform.""" + _setup_entities(devices, async_add_entities) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_BINARY_SENSORS), discover) + ) + + _setup_entities( + hass.data[DOMAIN][config_entry.entry_id][VS_BINARY_SENSORS], async_add_entities + ) + + +@callback +def _setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + entities = [] + for dev in devices: + if is_humidifier(dev.device_type): + entities.append(VeSyncOutOfWaterSensor(dev)) + entities.append(VeSyncWaterTankLiftedSensor(dev)) + else: + _LOGGER.warning( + "%s - Unknown device type - %s", dev.device_name, dev.device_type + ) + + async_add_entities(entities, update_before_add=True) + + +class VeSyncHumidifierBinarySensorEntity(VeSyncBaseEntity, BinarySensorEntity): + """Representation of a binary sensor describing diagnostics of a VeSync humidifier.""" + + def __init__(self, humidifier): + """Initialize the VeSync humidifier device.""" + super().__init__(humidifier) + self.smarthumidifier = humidifier + + @property + def entity_category(self): + """Return the diagnostic entity category.""" + return EntityCategory.DIAGNOSTIC + + +class VeSyncOutOfWaterSensor(VeSyncHumidifierBinarySensorEntity): + """Out of Water Sensor.""" + + @property + def unique_id(self): + """Return unique ID for out of water sensor on device.""" + return f"{super().unique_id}-out_of_water" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} out of water" + + @property + def is_on(self) -> bool: + """Return a value indicating whether the Humidifier is out of water.""" + return self.smarthumidifier.details["water_lacks"] + + +class VeSyncWaterTankLiftedSensor(VeSyncHumidifierBinarySensorEntity): + """Tank Lifted Sensor.""" + + @property + def unique_id(self): + """Return unique ID for water tank lifted sensor on device.""" + return f"{super().unique_id}-water_tank_lifted" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} water tank lifted" + + @property + def is_on(self) -> bool: + """Return a value indicating whether the Humidifier's water tank is lifted.""" + return self.smarthumidifier.details["water_tank_lifted"] diff --git a/custom_components/vesync/common.py b/custom_components/vesync/common.py new file mode 100644 index 0000000..5aa80f6 --- /dev/null +++ b/custom_components/vesync/common.py @@ -0,0 +1,140 @@ +"""Common utilities for VeSync Component.""" +import logging + +from homeassistant.helpers.entity import Entity, ToggleEntity + +from .const import ( + DOMAIN, + VS_BINARY_SENSORS, + VS_FANS, + VS_HUMIDIFIERS, + VS_LIGHTS, + VS_NUMBERS, + VS_SENSORS, + VS_SWITCHES, +) +from .pyvesync.vesyncfan import model_features + +_LOGGER = logging.getLogger(__name__) + + +def is_humidifier(device_type: str) -> bool: + """Return true if the device type is a humidifier.""" + return model_features(device_type)["module"].find("VeSyncHumid") > -1 + + +async def async_process_devices(hass, manager): + """Assign devices to proper component.""" + devices = { + VS_SWITCHES: [], + VS_FANS: [], + VS_LIGHTS: [], + VS_SENSORS: [], + VS_HUMIDIFIERS: [], + VS_NUMBERS: [], + VS_BINARY_SENSORS: [], + } + + await hass.async_add_executor_job(manager.update) + + if manager.fans: + for fan in manager.fans: + # VeSync classifies humidifiers as fans + if is_humidifier(fan.device_type): + devices[VS_HUMIDIFIERS].append(fan) + devices[VS_NUMBERS].append(fan) # for night light and mist level + devices[VS_SWITCHES].append(fan) # for automatic stop and display + devices[VS_SENSORS].append(fan) # for humidity sensor + devices[VS_BINARY_SENSORS].append( + fan + ) # for out of water and water tank lifted sensors + if fan.night_light: + devices[VS_LIGHTS].append(fan) # for night light + else: + devices[VS_FANS].append(fan) + _LOGGER.info("%d VeSync fans found", len(manager.fans)) + + if manager.bulbs: + devices[VS_LIGHTS].extend(manager.bulbs) + _LOGGER.info("%d VeSync lights found", len(manager.bulbs)) + + if manager.outlets: + devices[VS_SWITCHES].extend(manager.outlets) + # Expose outlets' power & energy usage as separate sensors + devices[VS_SENSORS].extend(manager.outlets) + _LOGGER.info("%d VeSync outlets found", len(manager.outlets)) + + if manager.switches: + for switch in manager.switches: + if not switch.is_dimmable(): + devices[VS_SWITCHES].append(switch) + else: + devices[VS_LIGHTS].append(switch) + _LOGGER.info("%d VeSync switches found", len(manager.switches)) + + return devices + + +class VeSyncBaseEntity(Entity): + """Base class for VeSync Entity Representations.""" + + def __init__(self, device): + """Initialize the VeSync device.""" + self.device = device + + @property + def base_unique_id(self): + """Return the ID of this device.""" + if isinstance(self.device.sub_device_no, int): + return f"{self.device.cid}{str(self.device.sub_device_no)}" + return self.device.cid + + @property + def unique_id(self): + """Return the ID of this device.""" + # The unique_id property may be overridden in subclasses, such as in sensors. Maintaining base_unique_id allows + # us to group related entities under a single device. + return self.base_unique_id + + @property + def base_name(self): + """Return the name of the device.""" + return self.device.device_name + + @property + def name(self): + """Return the name of the entity (may be overridden).""" + return self.base_name + + @property + def available(self) -> bool: + """Return True if device is available.""" + return self.device.connection_status == "online" + + @property + def device_info(self): + """Return device information.""" + return { + "identifiers": {(DOMAIN, self.base_unique_id)}, + "name": self.base_name, + "model": self.device.device_type, + "default_manufacturer": "VeSync", + "sw_version": self.device.current_firm_version, + } + + def update(self): + """Update vesync device.""" + self.device.update() + + +class VeSyncDevice(VeSyncBaseEntity, ToggleEntity): + """Base class for VeSync Device Representations.""" + + @property + def is_on(self): + """Return True if device is on.""" + return self.device.device_status == "on" + + def turn_off(self, **kwargs): + """Turn the device off.""" + self.device.turn_off() diff --git a/custom_components/vesync/config_flow.py b/custom_components/vesync/config_flow.py new file mode 100644 index 0000000..30e4b98 --- /dev/null +++ b/custom_components/vesync/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow utilities.""" +from collections import OrderedDict + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import DOMAIN +from .pyvesync.vesync import VeSync + + +class VeSyncFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + + def __init__(self): + """Instantiate config flow.""" + self._username = None + self._password = None + self.data_schema = OrderedDict() + self.data_schema[vol.Required(CONF_USERNAME)] = str + self.data_schema[vol.Required(CONF_PASSWORD)] = str + + @callback + def _show_form(self, errors=None): + """Show form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(self.data_schema), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow start.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if not user_input: + return self._show_form() + + self._username = user_input[CONF_USERNAME] + self._password = user_input[CONF_PASSWORD] + + manager = VeSync(self._username, self._password) + login = await self.hass.async_add_executor_job(manager.login) + await self.async_set_unique_id(f"{self._username}-{manager.account_id}") + self._abort_if_unique_id_configured() + + if not login: + return self._show_form(errors={"base": "invalid_auth"}) + + return self.async_create_entry( + title=self._username, + data={CONF_USERNAME: self._username, CONF_PASSWORD: self._password}, + ) diff --git a/custom_components/vesync/const.py b/custom_components/vesync/const.py new file mode 100644 index 0000000..c555a13 --- /dev/null +++ b/custom_components/vesync/const.py @@ -0,0 +1,33 @@ +"""Constants for VeSync Component.""" + +DOMAIN = "vesync" +VS_DISCOVERY = "vesync_discovery_{}" +SERVICE_UPDATE_DEVS = "update_devices" + +VS_SWITCHES = "switches" +VS_FANS = "fans" +VS_LIGHTS = "lights" +VS_SENSORS = "sensors" +VS_HUMIDIFIERS = "humidifiers" +VS_NUMBERS = "numbers" +VS_BINARY_SENSORS = "binary_sensors" +VS_MANAGER = "manager" + +DEV_TYPE_TO_HA = { + "LV-PUR131S": "fan", + "Core200S": "fan", + "Core300S": "fan", + "Core400S": "fan", + "Classic300S": "humidifier", + "ESD16": "walldimmer", + "ESWD16": "walldimmer", + "ESL100": "bulb-dimmable", + "ESL100CW": "bulb-tunable-white", + "wifi-switch-1.3": "outlet", + "ESW03-USA": "outlet", + "ESW01-EU": "outlet", + "ESW15-USA": "outlet", + "ESWL01": "switch", + "ESWL03": "switch", + "ESO15-TB": "outlet", +} diff --git a/custom_components/vesync/fan.py b/custom_components/vesync/fan.py new file mode 100644 index 0000000..43dd688 --- /dev/null +++ b/custom_components/vesync/fan.py @@ -0,0 +1,188 @@ +"""Support for VeSync fans.""" +import logging +import math + +from homeassistant.components.fan import SUPPORT_SET_SPEED, FanEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util.percentage import ( + int_states_in_range, + percentage_to_ranged_value, + ranged_value_to_percentage, +) + +from .common import VeSyncDevice +from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_FANS + +_LOGGER = logging.getLogger(__name__) + +FAN_MODE_AUTO = "auto" +FAN_MODE_SLEEP = "sleep" +# Fixme add other models +PRESET_MODES = { + "Core200S": [FAN_MODE_SLEEP], + "Core300S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "Core400S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], +} +SPEED_RANGE = (1, 3) # off is not included + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the VeSync fan platform.""" + + @callback + def discover(devices): + """Add new devices to platform.""" + _setup_entities(devices, async_add_entities) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_FANS), discover) + ) + + _setup_entities( + hass.data[DOMAIN][config_entry.entry_id][VS_FANS], async_add_entities + ) + + +@callback +def _setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + entities = [] + for dev in devices: + if DEV_TYPE_TO_HA.get(dev.device_type) == "fan": + entities.append(VeSyncFanHA(dev)) + else: + _LOGGER.warning( + "%s - Unknown device type - %s", dev.device_name, dev.device_type + ) + continue + + async_add_entities(entities, update_before_add=True) + + +class VeSyncFanHA(VeSyncDevice, FanEntity): + """Representation of a VeSync fan.""" + + def __init__(self, fan): + """Initialize the VeSync fan device.""" + super().__init__(fan) + self.smartfan = fan + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_SET_SPEED + + @property + def percentage(self): + """Return the current speed.""" + if ( + self.smartfan.mode == "manual" + and (current_level := self.smartfan.fan_level) is not None + ): + return ranged_value_to_percentage(SPEED_RANGE, current_level) + return None + + @property + def speed_count(self) -> int: + """Return the number of speeds the fan supports.""" + return int_states_in_range(SPEED_RANGE) + + @property + def preset_modes(self): + """Get the list of available preset modes.""" + return PRESET_MODES[self.device.device_type] + + @property + def preset_mode(self): + """Get the current preset mode.""" + if self.smartfan.mode in (FAN_MODE_AUTO, FAN_MODE_SLEEP): + return self.smartfan.mode + return None + + @property + def unique_info(self): + """Return the ID of this fan.""" + return self.smartfan.uuid + + @property + def extra_state_attributes(self): + """Return the state attributes of the fan.""" + attr = {} + + if hasattr(self.smartfan, "active_time"): + attr["active_time"] = self.smartfan.active_time + + if hasattr(self.smartfan, "screen_status"): + attr["screen_status"] = self.smartfan.screen_status + + if hasattr(self.smartfan, "child_lock"): + attr["child_lock"] = self.smartfan.child_lock + + if hasattr(self.smartfan, "night_light"): + attr["night_light"] = self.smartfan.night_light + + if hasattr(self.smartfan, "air_quality"): + attr["air_quality"] = self.smartfan.air_quality + + if hasattr(self.smartfan, "mode"): + attr["mode"] = self.smartfan.mode + + if hasattr(self.smartfan, "filter_life"): + attr["filter_life"] = self.smartfan.filter_life + + return attr + + def set_percentage(self, percentage): + """Set the speed of the device.""" + if percentage == 0: + self.smartfan.turn_off() + return + + if not self.smartfan.is_on: + self.smartfan.turn_on() + + self.smartfan.manual_mode() + self.smartfan.change_fan_speed( + math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage)) + ) + self.schedule_update_ha_state() + + def set_preset_mode(self, preset_mode): + """Set the preset mode of device.""" + if preset_mode not in self.preset_modes: + raise ValueError( + "{preset_mode} is not one of the valid preset modes: {self.preset_modes}" + ) + + if not self.smartfan.is_on: + self.smartfan.turn_on() + + if preset_mode == FAN_MODE_AUTO: + self.smartfan.auto_mode() + elif preset_mode == FAN_MODE_SLEEP: + self.smartfan.sleep_mode() + + self.schedule_update_ha_state() + + def turn_on( + self, + speed: str = None, + percentage: int = None, + preset_mode: str = None, + **kwargs, + ) -> None: + """Turn the device on.""" + if preset_mode: + self.set_preset_mode(preset_mode) + return + if percentage is None: + percentage = 50 + self.set_percentage(percentage) diff --git a/custom_components/vesync/humidifier.py b/custom_components/vesync/humidifier.py new file mode 100644 index 0000000..e7cf08f --- /dev/null +++ b/custom_components/vesync/humidifier.py @@ -0,0 +1,154 @@ +"""Support for VeSync humidifiers.""" +import logging + +from homeassistant.components.humidifier import HumidifierEntity +from homeassistant.components.humidifier.const import SUPPORT_MODES +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .common import VeSyncDevice, is_humidifier +from .const import DOMAIN, VS_DISCOVERY, VS_HUMIDIFIERS + +_LOGGER = logging.getLogger(__name__) + +MAX_HUMIDITY = 80 +MIN_HUMIDITY = 30 + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the VeSync humidifier platform.""" + + @callback + def discover(devices): + """Add new devices to platform.""" + _setup_entities(devices, async_add_entities) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_HUMIDIFIERS), discover) + ) + + _setup_entities( + hass.data[DOMAIN][config_entry.entry_id][VS_HUMIDIFIERS], async_add_entities + ) + + +@callback +def _setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + entities = [] + for dev in devices: + if is_humidifier(dev.device_type): + entities.append(VeSyncHumidifierHA(dev)) + else: + _LOGGER.warning( + "%s - Unknown device type - %s", dev.device_name, dev.device_type + ) + continue + + async_add_entities(entities, update_before_add=True) + + +class VeSyncHumidifierHA(VeSyncDevice, HumidifierEntity): + """Representation of a VeSync humidifier.""" + + _attr_max_humidity = MAX_HUMIDITY + _attr_min_humidity = MIN_HUMIDITY + + def __init__(self, humidifier): + """Initialize the VeSync humidifier device.""" + super().__init__(humidifier) + self.smarthumidifier = humidifier + + @property + def available_modes(self): + """Return the available mist modes.""" + return self.device.config_dict["mist_modes"] + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_MODES + + @property + def target_humidity(self): + """Return the humidity we try to reach.""" + return self.smarthumidifier.config["auto_target_humidity"] + + @property + def mode(self): + """Get the current preset mode.""" + if self.smarthumidifier.details["mode"] in self.available_modes: + return self.smarthumidifier.details["mode"] + return None + + @property + def is_on(self): + """Return True if humidifier is on.""" + return self.smarthumidifier.enabled # device_status is always on + + @property + def unique_info(self): + """Return the ID of this humidifier.""" + return self.smarthumidifier.uuid + + @property + def extra_state_attributes(self): + """Return the state attributes of the humidifier.""" + attr = {} + + if "water_lacks" in self.smarthumidifier.details: + attr["water_lacks"] = self.smarthumidifier.details["water_lacks"] + + if "humidity_high" in self.smarthumidifier.details: + attr["humidity_high"] = self.smarthumidifier.details["humidity_high"] + + if "water_tank_lifted" in self.smarthumidifier.details: + attr["water_tank_lifted"] = self.smarthumidifier.details[ + "water_tank_lifted" + ] + + if "automatic_stop_reach_target" in self.smarthumidifier.details: + attr["automatic_stop_reach_target"] = self.smarthumidifier.details[ + "automatic_stop_reach_target" + ] + + if "mist_level" in self.smarthumidifier.details: + attr["mist_level"] = self.smarthumidifier.details["mist_level"] + + return attr + + def set_humidity(self, humidity): + """Set the target humidity of the device.""" + if humidity not in range(self.min_humidity, self.max_humidity + 1): + raise ValueError( + "{humidity} is not between {self.min_humidity} and {self.max_humidity} (inclusive)" + ) + self.smarthumidifier.set_humidity(humidity) + self.schedule_update_ha_state() + + def set_mode(self, mode): + """Set the mode of the device.""" + if mode not in self.available_modes: + raise ValueError( + "{mode} is not one of the valid available modes: {self.available_modes}" + ) + if mode == "manual": + self.smarthumidifier.set_mist_level( + self.smarthumidifier.details["mist_level"] + ) + else: + self.smarthumidifier.set_humidity_mode(mode) + self.schedule_update_ha_state() + + def turn_on( + self, + **kwargs, + ) -> None: + """Turn the device on.""" + self.smarthumidifier.turn_on() diff --git a/custom_components/vesync/light.py b/custom_components/vesync/light.py new file mode 100644 index 0000000..afb8ab7 --- /dev/null +++ b/custom_components/vesync/light.py @@ -0,0 +1,251 @@ +"""Support for VeSync bulbs and wall dimmers.""" +import logging + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .common import VeSyncDevice, is_humidifier +from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_LIGHTS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up lights.""" + + @callback + def discover(devices): + """Add new devices to platform.""" + _setup_entities(devices, async_add_entities) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_LIGHTS), discover) + ) + + _setup_entities( + hass.data[DOMAIN][config_entry.entry_id][VS_LIGHTS], async_add_entities + ) + + +@callback +def _setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + entities = [] + for dev in devices: + if DEV_TYPE_TO_HA.get(dev.device_type) in ("walldimmer", "bulb-dimmable"): + entities.append(VeSyncDimmableLightHA(dev)) + elif DEV_TYPE_TO_HA.get(dev.device_type) in ("bulb-tunable-white",): + entities.append(VeSyncTunableWhiteLightHA(dev)) + elif is_humidifier(dev.device_type): + entities.append(VeSyncHumidifierNightLightHA(dev)) + else: + _LOGGER.debug( + "%s - Unknown device type - %s", dev.device_name, dev.device_type + ) + continue + + async_add_entities(entities, update_before_add=True) + + +def _vesync_brightness_to_ha(vesync_brightness): + try: + # check for validity of brightness value received + brightness_value = int(vesync_brightness) + except ValueError: + # deal if any unexpected/non numeric value + _LOGGER.debug( + "VeSync - received unexpected 'brightness' value from pyvesync api: %s", + vesync_brightness, + ) + return 0 + # convert percent brightness to ha expected range + return round((max(1, brightness_value) / 100) * 255) + + +def _ha_brightness_to_vesync(ha_brightness): + # get brightness from HA data + brightness = int(ha_brightness) + # ensure value between 1-255 + brightness = max(1, min(brightness, 255)) + # convert to percent that vesync api expects + brightness = round((brightness / 255) * 100) + # ensure value between 1-100 + brightness = max(1, min(brightness, 100)) + + return brightness + + +class VeSyncBaseLight(VeSyncDevice, LightEntity): + """Base class for VeSync Light Devices Representations.""" + + @property + def brightness(self): + """Get light brightness.""" + # get value from pyvesync library api, + return _vesync_brightness_to_ha(self.device.brightness) + + def turn_on(self, **kwargs): + """Turn the device on.""" + attribute_adjustment_only = False + # set white temperature + if self.color_mode in (COLOR_MODE_COLOR_TEMP,) and ATTR_COLOR_TEMP in kwargs: + # get white temperature from HA data + color_temp = int(kwargs[ATTR_COLOR_TEMP]) + # ensure value between min-max supported Mireds + color_temp = max(self.min_mireds, min(color_temp, self.max_mireds)) + # convert Mireds to Percent value that api expects + color_temp = round( + ((color_temp - self.min_mireds) / (self.max_mireds - self.min_mireds)) + * 100 + ) + # flip cold/warm to what pyvesync api expects + color_temp = 100 - color_temp + # ensure value between 0-100 + color_temp = max(0, min(color_temp, 100)) + # call pyvesync library api method to set color_temp + self.device.set_color_temp(color_temp) + # flag attribute_adjustment_only, so it doesn't turn_on the device redundantly + attribute_adjustment_only = True + # set brightness level + if ( + self.color_mode in (COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP) + and ATTR_BRIGHTNESS in kwargs + ): + # get brightness from HA data + brightness = _ha_brightness_to_vesync(kwargs[ATTR_BRIGHTNESS]) + self.device.set_brightness(brightness) + # flag attribute_adjustment_only, so it doesn't turn_on the device redundantly + attribute_adjustment_only = True + # check flag if should skip sending the turn_on command + if attribute_adjustment_only: + return + # send turn_on command to pyvesync api + self.device.turn_on() + + +class VeSyncDimmableLightHA(VeSyncBaseLight, LightEntity): + """Representation of a VeSync dimmable light device.""" + + @property + def color_mode(self): + """Set color mode for this entity.""" + return COLOR_MODE_BRIGHTNESS + + @property + def supported_color_modes(self): + """Flag supported color_modes (in an array format).""" + return [COLOR_MODE_BRIGHTNESS] + + +class VeSyncTunableWhiteLightHA(VeSyncBaseLight, LightEntity): + """Representation of a VeSync Tunable White Light device.""" + + @property + def color_temp(self): + """Get device white temperature.""" + # get value from pyvesync library api, + result = self.device.color_temp_pct + try: + # check for validity of brightness value received + color_temp_value = int(result) + except ValueError: + # deal if any unexpected/non numeric value + _LOGGER.debug( + "VeSync - received unexpected 'color_temp_pct' value from pyvesync api: %s", + result, + ) + return 0 + # flip cold/warm + color_temp_value = 100 - color_temp_value + # ensure value between 0-100 + color_temp_value = max(0, min(color_temp_value, 100)) + # convert percent value to Mireds + color_temp_value = round( + self.min_mireds + + ((self.max_mireds - self.min_mireds) / 100 * color_temp_value) + ) + # ensure value between minimum and maximum Mireds + return max(self.min_mireds, min(color_temp_value, self.max_mireds)) + + @property + def min_mireds(self): + """Set device coldest white temperature.""" + return 154 # 154 Mireds ( 1,000,000 divided by 6500 Kelvin = 154 Mireds) + + @property + def max_mireds(self): + """Set device warmest white temperature.""" + return 370 # 370 Mireds ( 1,000,000 divided by 2700 Kelvin = 370 Mireds) + + @property + def color_mode(self): + """Set color mode for this entity.""" + return COLOR_MODE_COLOR_TEMP + + @property + def supported_color_modes(self): + """Flag supported color_modes (in an array format).""" + return [COLOR_MODE_COLOR_TEMP] + + +class VeSyncHumidifierNightLightHA(VeSyncDimmableLightHA): + """Representation of the night light on a VeSync humidifier.""" + + def __init__(self, humidifier): + """Initialize the VeSync humidifier device.""" + super().__init__(humidifier) + self.smarthumidifier = humidifier + + @property + def unique_id(self): + """Return the ID of this device.""" + return f"{super().unique_id}-night-light" + + @property + def name(self): + """Return the name of the device.""" + return f"{super().name} night light" + + @property + def brightness(self): + """Get night light brightness.""" + # get value from pyvesync library api, + return _vesync_brightness_to_ha( + self.smarthumidifier.details["night_light_brightness"] + ) + + @property + def is_on(self): + """Return True if night light is on.""" + return self.smarthumidifier.details["night_light_brightness"] > 0 + + @property + def entity_category(self): + """Return the configuration entity category.""" + return EntityCategory.CONFIG + + def turn_on(self, **kwargs): + """Turn the night light on.""" + if ATTR_BRIGHTNESS in kwargs: + brightness = _ha_brightness_to_vesync(kwargs[ATTR_BRIGHTNESS]) + self.smarthumidifier.set_night_light_brightness(brightness) + else: + self.smarthumidifier.set_night_light_brightness(100) + + def turn_off(self, **kwargs): + """Turn the night light off.""" + self.smarthumidifier.set_night_light_brightness(0) diff --git a/custom_components/vesync/manifest.json b/custom_components/vesync/manifest.json new file mode 100644 index 0000000..fc99254 --- /dev/null +++ b/custom_components/vesync/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "vesync", + "name": "VeSync", + "documentation": "https://www.home-assistant.io/integrations/vesync", + "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], + "config_flow": true, + "iot_class": "cloud_polling", + "version": "0.1.2", + "issue_tracker": "https://github.com/vlebourl/vesync-bpo" +} diff --git a/custom_components/vesync/number.py b/custom_components/vesync/number.py new file mode 100644 index 0000000..4711be4 --- /dev/null +++ b/custom_components/vesync/number.py @@ -0,0 +1,155 @@ +"""Support for number settings on VeSync devices.""" +import logging + +from homeassistant.components.number import NumberEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .common import VeSyncBaseEntity, is_humidifier +from .const import DOMAIN, VS_DISCOVERY, VS_NUMBERS + +MAX_HUMIDITY = 80 +MIN_HUMIDITY = 30 + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up numbers.""" + + @callback + def discover(devices): + """Add new devices to platform.""" + _setup_entities(devices, async_add_entities) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_NUMBERS), discover) + ) + + _setup_entities( + hass.data[DOMAIN][config_entry.entry_id][VS_NUMBERS], async_add_entities + ) + + +@callback +def _setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + entities = [] + for dev in devices: + if is_humidifier(dev.device_type): + entities.extend( + ( + VeSyncHumidifierMistLevelHA(dev), + VeSyncHumidifierTargetLevelHA(dev), + ) + ) + + else: + _LOGGER.debug( + "%s - Unknown device type - %s", dev.device_name, dev.device_type + ) + continue + + async_add_entities(entities, update_before_add=True) + + +class VeSyncHumidifierNumberEntity(VeSyncBaseEntity, NumberEntity): + """Representation of a number for configuring a VeSync humidifier.""" + + def __init__(self, humidifier): + """Initialize the VeSync humidifier device.""" + super().__init__(humidifier) + self.smarthumidifier = humidifier + + @property + def entity_category(self): + """Return the diagnostic entity category.""" + return EntityCategory.CONFIG + + +class VeSyncHumidifierMistLevelHA(VeSyncHumidifierNumberEntity): + """Representation of the mist level of a VeSync humidifier.""" + + @property + def unique_id(self): + """Return the ID of this device.""" + return f"{super().unique_id}-mist-level" + + @property + def name(self): + """Return the name of the device.""" + return f"{super().name} mist level" + + @property + def value(self): + """Return the mist level.""" + return self.device.details["mist_virtual_level"] + + @property + def min_value(self) -> float: + """Return the minimum mist level.""" + return self.device.config_dict["mist_levels"][0] + + @property + def max_value(self) -> float: + """Return the maximum mist level.""" + return self.device.config_dict["mist_levels"][-1] + + @property + def step(self) -> float: + """Return the steps for the mist level.""" + return 1.0 + + @property + def extra_state_attributes(self): + """Return the state attributes of the humidifier.""" + return {"mist levels": self.device.config_dict["mist_levels"]} + + def set_value(self, value): + """Set the mist level.""" + self.device.set_mist_level(int(value)) + + +class VeSyncHumidifierTargetLevelHA(VeSyncHumidifierNumberEntity): + """Representation of the target humidity level of a VeSync humidifier.""" + + @property + def unique_id(self): + """Return the ID of this device.""" + return f"{super().unique_id}-target-level" + + @property + def name(self): + """Return the name of the device.""" + return f"{super().name} target level" + + @property + def value(self): + """Return the current target humidity level.""" + return self.device.config["auto_target_humidity"] + + @property + def min_value(self) -> float: + """Return the minimum humidity level.""" + return MIN_HUMIDITY + + @property + def max_value(self) -> float: + """Return the maximum humidity level.""" + return MAX_HUMIDITY + + @property + def step(self) -> float: + """Return the humidity change step.""" + return 1.0 + + def set_value(self, value): + """Set the target humidity level.""" + self.device.set_humidity(int(value)) diff --git a/custom_components/vesync/pyvesync/__init__.py b/custom_components/vesync/pyvesync/__init__.py new file mode 100755 index 0000000..3fb317f --- /dev/null +++ b/custom_components/vesync/pyvesync/__init__.py @@ -0,0 +1,6 @@ +"""VeSync API Library.""" +import logging + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)5s - %(message)s" +) diff --git a/custom_components/vesync/pyvesync/__pycache__/__init__.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..22f9bfc Binary files /dev/null and b/custom_components/vesync/pyvesync/__pycache__/__init__.cpython-39.pyc differ diff --git a/custom_components/vesync/pyvesync/__pycache__/helpers.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/helpers.cpython-39.pyc new file mode 100644 index 0000000..5a92898 Binary files /dev/null and b/custom_components/vesync/pyvesync/__pycache__/helpers.cpython-39.pyc differ diff --git a/custom_components/vesync/pyvesync/__pycache__/vesync.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/vesync.cpython-39.pyc new file mode 100644 index 0000000..695c15b Binary files /dev/null and b/custom_components/vesync/pyvesync/__pycache__/vesync.cpython-39.pyc differ diff --git a/custom_components/vesync/pyvesync/__pycache__/vesyncbasedevice.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/vesyncbasedevice.cpython-39.pyc new file mode 100644 index 0000000..951a670 Binary files /dev/null and b/custom_components/vesync/pyvesync/__pycache__/vesyncbasedevice.cpython-39.pyc differ diff --git a/custom_components/vesync/pyvesync/__pycache__/vesyncbulb.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/vesyncbulb.cpython-39.pyc new file mode 100644 index 0000000..52166c0 Binary files /dev/null and b/custom_components/vesync/pyvesync/__pycache__/vesyncbulb.cpython-39.pyc differ diff --git a/custom_components/vesync/pyvesync/__pycache__/vesyncfan.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/vesyncfan.cpython-39.pyc new file mode 100644 index 0000000..cb0931c Binary files /dev/null and b/custom_components/vesync/pyvesync/__pycache__/vesyncfan.cpython-39.pyc differ diff --git a/custom_components/vesync/pyvesync/__pycache__/vesyncoutlet.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/vesyncoutlet.cpython-39.pyc new file mode 100644 index 0000000..b3a0ae9 Binary files /dev/null and b/custom_components/vesync/pyvesync/__pycache__/vesyncoutlet.cpython-39.pyc differ diff --git a/custom_components/vesync/pyvesync/__pycache__/vesyncswitch.cpython-39.pyc b/custom_components/vesync/pyvesync/__pycache__/vesyncswitch.cpython-39.pyc new file mode 100644 index 0000000..c832d05 Binary files /dev/null and b/custom_components/vesync/pyvesync/__pycache__/vesyncswitch.cpython-39.pyc differ diff --git a/custom_components/vesync/pyvesync/helpers.py b/custom_components/vesync/pyvesync/helpers.py new file mode 100644 index 0000000..6bc6bf0 --- /dev/null +++ b/custom_components/vesync/pyvesync/helpers.py @@ -0,0 +1,241 @@ +"""Helper functions for VeSync API.""" + +import hashlib +import logging +import time + +import requests + +logger = logging.getLogger(__name__) + +API_BASE_URL = "https://smartapi.vesync.com" +API_RATE_LIMIT = 30 +API_TIMEOUT = 5 + +DEFAULT_TZ = "America/New_York" +DEFAULT_REGION = "US" + +APP_VERSION = "2.8.6" +PHONE_BRAND = "SM N9005" +PHONE_OS = "Android" +MOBILE_ID = "1234567890123456" +USER_TYPE = "1" +BYPASS_APP_V = "VeSync 3.0.51" + + +class Helpers: + """VeSync Helper Functions.""" + + @staticmethod + def req_headers(manager) -> dict: + """Build header for api requests.""" + return { + "accept-language": "en", + "accountId": manager.account_id, + "appVersion": APP_VERSION, + "content-type": "application/json", + "tk": manager.token, + "tz": manager.time_zone, + } + + @staticmethod + def req_body_base(manager) -> dict: + """Return universal keys for body of api requests.""" + return {"timeZone": manager.time_zone, "acceptLanguage": "en"} + + @staticmethod + def req_body_auth(manager) -> dict: + """Keys for authenticating api requests.""" + return {"accountID": manager.account_id, "token": manager.token} + + @staticmethod + def req_body_details() -> dict: + """Detail keys for api requests.""" + return { + "appVersion": APP_VERSION, + "phoneBrand": PHONE_BRAND, + "phoneOS": PHONE_OS, + "traceId": str(int(time.time())), + } + + @classmethod + def req_body(cls, manager, type_) -> dict: + """Builder for body of api requests.""" + body = {} + + if type_ == "login": + body = {**cls.req_body_base(manager), **cls.req_body_details()} + body["email"] = manager.username + body["password"] = cls.hash_password(manager.password) + body["devToken"] = "" + body["userType"] = USER_TYPE + body["method"] = "login" + elif type_ == "devicedetail": + body = { + **cls.req_body_base(manager), + **cls.req_body_auth(manager), + **cls.req_body_details(), + } + body["method"] = "devicedetail" + body["mobileId"] = MOBILE_ID + elif type_ == "devicelist": + body = { + **cls.req_body_base(manager), + **cls.req_body_auth(manager), + **cls.req_body_details(), + } + body["method"] = "devices" + body["pageNo"] = "1" + body["pageSize"] = "100" + elif type_ == "devicestatus": + body = {**cls.req_body_base(manager), **cls.req_body_auth(manager)} + elif type_ == "energy_week": + body = { + **cls.req_body_base(manager), + **cls.req_body_auth(manager), + **cls.req_body_details(), + } + body["method"] = "energyweek" + body["mobileId"] = MOBILE_ID + elif type_ == "energy_month": + body = { + **cls.req_body_base(manager), + **cls.req_body_auth(manager), + **cls.req_body_details(), + } + body["method"] = "energymonth" + body["mobileId"] = MOBILE_ID + elif type_ == "energy_year": + body = { + **cls.req_body_base(manager), + **cls.req_body_auth(manager), + **cls.req_body_details(), + } + body["method"] = "energyyear" + body["mobileId"] = MOBILE_ID + elif type_ == "bypass": + body = { + **cls.req_body_base(manager), + **cls.req_body_auth(manager), + **cls.req_body_details(), + } + body["method"] = "bypass" + elif type_ == "bypass_config": + body = { + **cls.req_body_base(manager), + **cls.req_body_auth(manager), + **cls.req_body_details(), + } + body["method"] = "firmwareUpdateInfo" + + return body + + @staticmethod + def calculate_hex(hex_string) -> float: + """Credit for conversion to itsnotlupus/vesync_wsproxy.""" + hex_conv = hex_string.split(":") + return (int(hex_conv[0], 16) + int(hex_conv[1], 16)) / 8192 + + @staticmethod + def hash_password(string) -> str: + """Encode password.""" + return hashlib.md5(string.encode("utf-8")).hexdigest() + + @staticmethod + def call_api( + api: str, method: str, json: dict = None, headers: dict = None + ) -> tuple: + """Make API calls by passing endpoint, header and body.""" + response = None + status_code = None + + try: + logger.debug("[%s] calling '%s' api", method, api) + if method.lower() == "get": + r = requests.get( + API_BASE_URL + api, json=json, headers=headers, timeout=API_TIMEOUT + ) + elif method.lower() == "post": + r = requests.post( + API_BASE_URL + api, json=json, headers=headers, timeout=API_TIMEOUT + ) + elif method.lower() == "put": + r = requests.put( + API_BASE_URL + api, json=json, headers=headers, timeout=API_TIMEOUT + ) + except requests.exceptions.RequestException as e: + logger.debug(e) + except Exception as e: + logger.debug(e) + else: + if r.status_code == 200: + status_code = 200 + if r.content: + response = r.json() + else: + logger.debug("Unable to fetch %s%s", API_BASE_URL, api) + return response, status_code + + @staticmethod + def code_check(r: dict) -> bool: + """Test if code == 0 for successful API call.""" + return isinstance(r, dict) and r.get("code") == 0 + + @staticmethod + def build_details_dict(r: dict) -> dict: + """Build details dictionary from API response.""" + return { + "active_time": r.get("activeTime", 0), + "energy": r.get("energy", 0), + "night_light_status": r.get("nightLightStatus", None), + "night_light_brightness": r.get("nightLightBrightness", None), + "night_light_automode": r.get("nightLightAutomode", None), + "power": r.get("power", 0), + "voltage": r.get("voltage", 0), + } + + @staticmethod + def build_energy_dict(r: dict) -> dict: + """Build energy dictionary from API response.""" + return { + "energy_consumption_of_today": r.get("energyConsumptionOfToday", 0), + "cost_per_kwh": r.get("costPerKWH", 0), + "max_energy": r.get("maxEnergy", 0), + "total_energy": r.get("totalEnergy", 0), + "currency": r.get("currency", 0), + "data": r.get("data", 0), + } + + @staticmethod + def build_config_dict(r: dict) -> dict: + """Build configuration dictionary from API response.""" + if r.get("threshold") is not None: + threshold = r.get("threshold") + else: + threshold = r.get("thresHold") + return { + "current_firmware_version": r.get("currentFirmVersion"), + "latest_firmware_version": r.get("latestFirmVersion"), + "maxPower": r.get("maxPower"), + "threshold": threshold, + "power_protection": r.get("powerProtectionStatus"), + "energy_saving_status": r.get("energySavingStatus"), + } + + @classmethod + def bypass_body_v2(cls, manager): + """Build body dict for bypass calls.""" + bdy = {} + bdy.update(**cls.req_body(manager, "bypass")) + bdy["method"] = "bypassV2" + bdy["debugMode"] = False + bdy["deviceRegion"] = DEFAULT_REGION + return bdy + + @staticmethod + def bypass_header(): + """Build bypass header dict.""" + return { + "Content-Type": "application/json; charset=UTF-8", + "User-Agent": "okhttp/3.12.1", + } diff --git a/custom_components/vesync/pyvesync/vesync.py b/custom_components/vesync/pyvesync/vesync.py new file mode 100755 index 0000000..a3b058f --- /dev/null +++ b/custom_components/vesync/pyvesync/vesync.py @@ -0,0 +1,302 @@ +"""VeSync API Device Library.""" + +from itertools import chain +import logging +import re +import time +from typing import Tuple + +from . import ( + vesyncbulb as bulb_mods, + vesyncfan as fan_mods, + vesyncoutlet as outlet_mods, + vesyncswitch as switch_mods, +) +from .helpers import Helpers +from .vesyncbasedevice import VeSyncBaseDevice + +logger = logging.getLogger(__name__) + +API_RATE_LIMIT: int = 30 +DEFAULT_TZ: str = "America/New_York" + +DEFAULT_ENER_UP_INT: int = 21600 + + +def object_factory(dev_type, config, manager) -> Tuple[str, VeSyncBaseDevice]: + """Get device type and instantiate class.""" + + def fans(dev_type, config, manager): + fan_cls = fan_mods.fan_modules[dev_type] + fan_obj = getattr(fan_mods, fan_cls) + return "fans", fan_obj(config, manager) + + def outlets(dev_type, config, manager): + outlet_cls = outlet_mods.outlet_modules[dev_type] + outlet_obj = getattr(outlet_mods, outlet_cls) + return "outlets", outlet_obj(config, manager) + + def switches(dev_type, config, manager): + switch_cls = switch_mods.switch_modules[dev_type] + switch_obj = getattr(switch_mods, switch_cls) + return "switches", switch_obj(config, manager) + + def bulbs(dev_type, config, manager): + bulb_cls = bulb_mods.bulb_modules[dev_type] + bulb_obj = getattr(bulb_mods, bulb_cls) + return "bulbs", bulb_obj(config, manager) + + if dev_type in fan_mods.fan_modules: + type_str, dev_obj = fans(dev_type, config, manager) + elif dev_type in outlet_mods.outlet_modules: + type_str, dev_obj = outlets(dev_type, config, manager) + elif dev_type in switch_mods.switch_modules: + type_str, dev_obj = switches(dev_type, config, manager) + elif dev_type in bulb_mods.bulb_modules: + type_str, dev_obj = bulbs(dev_type, config, manager) + else: + logger.debug( + "Unknown device named %s model %s", + config.get("deviceName", ""), + config.get("deviceType", ""), + ) + type_str = "unknown" + dev_obj = None + return type_str, dev_obj + + +class VeSync: + """VeSync API functions.""" + + def __init__(self, username, password, time_zone=DEFAULT_TZ, debug=False): + """Initialize VeSync class with username, password and time zone.""" + self.debug = debug + if debug: + logger.setLevel(logging.DEBUG) + self.username = username + self.password = password + self.token = None + self.account_id = None + self.devices = None + self.enabled = False + self.update_interval = API_RATE_LIMIT + self.last_update_ts = None + self.in_process = False + self._energy_update_interval = DEFAULT_ENER_UP_INT + self._energy_check = True + self._dev_list = {} + self.outlets = [] + self.switches = [] + self.fans = [] + self.bulbs = [] + self.scales = [] + + self._dev_list = { + "fans": self.fans, + "outlets": self.outlets, + "switches": self.switches, + "bulbs": self.bulbs, + } + + if isinstance(time_zone, str) and time_zone: + reg_test = r"[^a-zA-Z/_]" + if bool(re.search(reg_test, time_zone)): + self.time_zone = DEFAULT_TZ + logger.debug("Invalid characters in time zone - %s", time_zone) + else: + self.time_zone = time_zone + else: + self.time_zone = DEFAULT_TZ + logger.debug("Time zone is not a string") + + @property + def energy_update_interval(self) -> int: + """Return energy update interval.""" + return self._energy_update_interval + + @energy_update_interval.setter + def energy_update_interval(self, new_energy_update: int) -> None: + """Set energy update interval in seconds.""" + if new_energy_update > 0: + self._energy_update_interval = new_energy_update + + @staticmethod + def remove_dev_test(device, new_list: list) -> bool: + """Test if device should be removed - False = Remove.""" + if isinstance(new_list, list) and device.cid: + for item in new_list: + device_found = False + if "cid" in item: + if device.cid == item["cid"]: + device_found = True + break + else: + logger.debug("No cid found in - %s", str(item)) + if not device_found: + logger.debug( + "Device removed - %s - %s", device.device_name, device.device_type + ) + return False + return True + + def add_dev_test(self, new_dev: dict) -> bool: + """Test if new device should be added - True = Add.""" + if "cid" in new_dev: + for _, v in self._dev_list.items(): + for dev in v: + if ( + dev.cid == new_dev.get("cid") + and new_dev.get("subDeviceNo", 0) == dev.sub_device_no + ): + return False + return True + + def remove_old_devices(self, devices: list) -> bool: + """Remove devices not found in device list return.""" + for k, v in self._dev_list.items(): + before = len(v) + v[:] = [x for x in v if self.remove_dev_test(x, devices)] + after = len(v) + if before != after: + logger.debug("%s %s removed", str(before - after), k) + return True + + @staticmethod + def set_dev_id(devices: list) -> list: + """Correct devices without cid or uuid.""" + dev_rem = [] + for dev_num, dev in enumerate(devices): + if dev.get("cid") is None: + if dev.get("macID") is not None: + dev["cid"] = dev["macID"] + elif dev.get("uuid") is not None: + dev["cid"] = dev["uuid"] + else: + dev_rem.append(dev_num) + logger.warning("Device with no ID - %s", dev.get("deviceName")) + if dev_rem: + devices = [i for j, i in enumerate(devices) if j not in dev_rem] + return devices + + def process_devices(self, dev_list: list) -> bool: + """Instantiate Device Objects.""" + devices = VeSync.set_dev_id(dev_list) + + num_devices = sum( + len(v) if isinstance(v, list) else 1 for _, v in self._dev_list.items() + ) + + if not devices: + logger.warning("No devices found in api return") + return False + if num_devices == 0: + logger.debug("New device list initialized") + else: + self.remove_old_devices(devices) + + devices[:] = [x for x in devices if self.add_dev_test(x)] + + detail_keys = ["deviceType", "deviceName", "deviceStatus"] + for dev in devices: + if any(k not in dev for k in detail_keys): + logger.debug("Error adding device") + continue + dev_type = dev.get("deviceType") + try: + device_str, device_obj = object_factory(dev_type, dev, self) + device_list = getattr(self, device_str) + device_list.append(device_obj) + except AttributeError as err: + logger.debug("Error - %s", err) + logger.debug("%s device not added", dev_type) + continue + + return True + + def get_devices(self) -> bool: + """Return tuple listing outlets, switches, and fans of devices.""" + if not self.enabled: + return False + + self.in_process = True + proc_return = False + response, _ = Helpers.call_api( + "/cloud/v1/deviceManaged/devices", + "post", + headers=Helpers.req_headers(self), + json=Helpers.req_body(self, "devicelist"), + ) + + if response and Helpers.code_check(response): + if "result" in response and "list" in response["result"]: + device_list = response["result"]["list"] + if self.debug: + logger.debug(str(device_list)) + proc_return = self.process_devices(device_list) + else: + logger.error("Device list in response not found") + else: + logger.warning("Error retrieving device list") + + self.in_process = False + + return proc_return + + def login(self) -> bool: + """Return True if log in request succeeds.""" + user_check = isinstance(self.username, str) and len(self.username) > 0 + pass_check = isinstance(self.password, str) and len(self.password) > 0 + if not user_check: + logger.error("Username invalid") + return False + if not pass_check: + logger.error("Password invalid") + return False + + response, _ = Helpers.call_api( + "/cloud/v1/user/login", "post", json=Helpers.req_body(self, "login") + ) + + if Helpers.code_check(response) and "result" in response: + self.token = response.get("result").get("token") + self.account_id = response.get("result").get("accountID") + self.enabled = True + + return True + logger.error("Error logging in with username and password") + return False + + def device_time_check(self) -> bool: + """Test if update interval has been exceeded.""" + return ( + self.last_update_ts is None + or (time.time() - self.last_update_ts) > self.update_interval + ) + + def update(self) -> None: + """Fetch updated information about devices.""" + if self.device_time_check(): + + if not self.enabled: + logger.error("Not logged in to VeSync") + return + self.get_devices() + + devices = list(self._dev_list.values()) + + for device in chain(*devices): + device.update() + + self.last_update_ts = time.time() + + def update_energy(self, bypass_check=False) -> None: + """Fetch updated energy information about devices.""" + if self.outlets: + for outlet in self.outlets: + outlet.update_energy(bypass_check) + + def update_all_devices(self) -> None: + """Run get_details() for each device.""" + devices = list(self._dev_list.keys()) + for dev in chain(*devices): + dev.get_details() diff --git a/custom_components/vesync/pyvesync/vesyncbasedevice.py b/custom_components/vesync/pyvesync/vesyncbasedevice.py new file mode 100644 index 0000000..f9799e4 --- /dev/null +++ b/custom_components/vesync/pyvesync/vesyncbasedevice.py @@ -0,0 +1,117 @@ +"""Base class for all VeSync devices.""" + +import collections +import json +import logging +from typing import Optional, Union + +logger = logging.getLogger(__name__) + + +class VeSyncBaseDevice: + """Properties shared across all VeSync devices.""" + + def __init__(self, details: dict, manager): + """Initialize VeSync device base class.""" + self.manager = manager + if "cid" in details and details["cid"] is not None: + self.device_name: str = details.get("deviceName", None) + self.device_image: Optional[str] = details.get("deviceImg", None) + self.cid: str = details.get("cid", None) + self.connection_status: str = details.get("connectionStatus", None) + self.connection_type: Optional[str] = details.get("connectionType", None) + self.device_type: str = details.get("deviceType", None) + self.type: str = details.get("type", None) + self.uuid: Optional[str] = details.get("uuid", None) + self.config_module: str = details.get("configModule", None) + self.mac_id: Optional[str] = details.get("macID", None) + self.mode: Optional[str] = details.get("mode", None) + self.speed: Union[str, int, None] = details.get("speed", None) + self.extension = details.get("extension", None) + self.current_firm_version = details.get("currentFirmVersion", None) + self.sub_device_no = details.get("subDeviceNo", 0) + self.config: dict = {} + if isinstance(details.get("extension"), dict): + ext = details["extension"] + self.speed = ext.get("fanSpeedLevel") + self.mode = ext.get("mode") + if self.connection_status != "online": + self.device_status = "off" + else: + self.device_status = details.get("deviceStatus", None) + + else: + logger.error("No cid found for %s", self.__class__.__name__) + + def __eq__(self, other): + """Use device CID and subdevice number to test equality.""" + return bool(other.cid == self.cid and other.sub_device_no == self.sub_device_no) + + def __hash__(self): + """Use CID and sub-device number to make device hash.""" + if isinstance(self.sub_device_no, int) and self.sub_device_no > 0: + return hash(self.cid + str(self.sub_device_no)) + return hash(self.cid) + + def __str__(self): + """Use device info for string represtation of class.""" + return f"Device Name: {self.device_name}, \ + Device Type: {self.device_type},\ + SubDevice No.: {self.sub_device_no},\ + Status: {self.device_status}" + + def __repr__(self): + """Representation of device details.""" + return f"DevClass: {self.__class__.__name__},\ + Name:{self.device_name}, Device No: {self.sub_device_no},\ + DevStatus: {self.device_status}, CID: {self.cid}" + + @property + def is_on(self) -> bool: + """Return true if device is on.""" + if self.device_status == "on": + return True + return False + + @property + def firmware_update(self) -> bool: + """Return True if firmware update available.""" + cfv = self.config.get("current_firmware_version") + lfv = self.config.get("latest_firmware_version") + if cfv is not None and lfv is not None: + if cfv != lfv: + return True + else: + logger.debug("Call device.get_config() to get firmware versions") + return False + + def display(self) -> None: + """Print formatted device info to stdout.""" + disp = [ + ("Device Name:", self.device_name), + ("Model: ", self.device_type), + ("Subdevice No: ", str(self.sub_device_no)), + ("Status: ", self.device_status), + ("Online: ", self.connection_status), + ("Type: ", self.type), + ("CID: ", self.cid), + ] + if self.uuid is not None: + disp.append(("UUID: ", self.uuid)) + disp1 = collections.OrderedDict(disp) + for k, v in disp1.items(): + print(f"{k:.<15} {v:<15}") + + def displayJSON(self) -> str: + """JSON API for device details.""" + return json.dumps( + { + "Device Name": self.device_name, + "Model": self.device_type, + "Subdevice No": str(self.sub_device_no), + "Status": self.device_status, + "Online": self.connection_status, + "Type": self.type, + "CID": self.cid, + } + ) diff --git a/custom_components/vesync/pyvesync/vesyncbulb.py b/custom_components/vesync/pyvesync/vesyncbulb.py new file mode 100644 index 0000000..d9315a7 --- /dev/null +++ b/custom_components/vesync/pyvesync/vesyncbulb.py @@ -0,0 +1,378 @@ +"""Etekcity Smart Light Bulb.""" + +from abc import ABCMeta, abstractmethod +import json +import logging +from typing import Dict, Union + +from .helpers import Helpers as helpers +from .vesyncbasedevice import VeSyncBaseDevice + +logger = logging.getLogger(__name__) + + +# Possible features - dimmable, color_temp, rgb_shift +feature_dict: dict = { + "ESL100": {"module": "VeSyncBulbESL100", "features": ["dimmable"]}, + "ESL100CW": { + "module": "VeSyncBulbESL100CW", + "features": ["dimmable", "color_temp"], + }, +} + + +bulb_modules: dict = {k: v["module"] for k, v in feature_dict.items()} + +__all__: list = list(bulb_modules.values()) + ["bulb_modules"] + + +def pct_to_kelvin(pct: float, max_k: int = 6500, min_k: int = 2700) -> float: + """Convert percent to kelvin.""" + return ((max_k - min_k) * pct / 100) + min_k + + +class VeSyncBulb(VeSyncBaseDevice): + """Base class for VeSync Bulbs.""" + + __metaclass__ = ABCMeta + + def __init__(self, details: Dict[str, Union[str, list]], manager): + """Initialize VeSync smart bulb base class.""" + super().__init__(details, manager) + self._brightness = 0 + self._color_temp = 0 + self.features = feature_dict.get(self.device_type, {}).get("features") + if self.features is None: + logger.error("No configuration set for - %s", self.device_name) + raise RuntimeError(f"No configuration set for - {self.device_name}") + + @property + def brightness(self) -> int: + """Return brightness of vesync bulb.""" + if self.dimmable_feature and self._brightness is not None: + return self._brightness + return 0 + + @property + def color_temp_kelvin(self) -> int: + """Return Color Temp of bulb if supported in Kelvin.""" + if self.color_temp_feature and self._color_temp is not None: + return int(pct_to_kelvin(self._color_temp)) + return 0 + + @property + def color_temp_pct(self) -> int: + """Return color temperature of bulb in percent.""" + if self.color_temp_feature and self._color_temp is not None: + return int(self._color_temp) + return 0 + + @property + def dimmable_feature(self) -> bool: + """Return true if dimmable bulb.""" + return "dimmable" in self.features + + @property + def color_temp_feature(self) -> bool: + """Return true if bulb supports color temperature changes.""" + return "color_temp" in feature_dict[self.device_type] + + @property + def rgb_shift_feature(self) -> bool: + """Return True if bulb supports changing color.""" + return "rgb_shift" in feature_dict[self.device_type] + + @abstractmethod + def get_details(self) -> None: + """Get vesync bulb details.""" + + @abstractmethod + def toggle(self, status: str) -> bool: + """Toggle vesync lightbulb.""" + + @abstractmethod + def get_config(self) -> None: + """Call api to get configuration details and firmware.""" + + def turn_on(self) -> bool: + """Turn on vesync bulbs.""" + return self._toggle("on") + + def turn_off(self) -> bool: + """Turn off vesync bulbs.""" + return self._toggle("off") + + def _toggle(self, state: str, warning: str): + if self.toggle(state): + self.device_status = state + return True + logger.warning("Error turning %s %s", self.device_name, state) + return False + + def update(self) -> None: + """Update bulb details.""" + self.get_details() + + def display(self) -> None: + """Return formatted bulb info to stdout.""" + super().display() + if self.connection_status == "online" and self.dimmable_feature: + disp1 = [("Brightness: ", self.brightness, "%")] + for line in disp1: + print(f"{line[0]:.<17} {line[1]} {line[2]}") + + def displayJSON(self) -> str: + """Return bulb device info in JSON format.""" + sup = super().displayJSON() + sup_val = json.loads(sup) + if self.connection_status == "online": + if self.dimmable_feature: + sup_val.update({"Brightness": str(self.brightness)}) + if self.color_temp_feature: + sup_val.update({"Kelvin": str(self.color_temp_kelvin)}) + return sup_val + + +class VeSyncBulbESL100(VeSyncBulb): + """Object to hold VeSync ESL100 light bulb.""" + + def __init__(self, details, manager): + """Initialize Etekcity ESL100 Dimmable Bulb.""" + super().__init__(details, manager) + self.details: dict = {} + + def get_details(self) -> None: + """Get details of dimmable bulb.""" + body = helpers.req_body(self.manager, "devicedetail") + body["uuid"] = self.uuid + r, _ = helpers.call_api( + "/SmartBulb/v1/device/devicedetail", + "post", + headers=helpers.req_headers(self.manager), + json=body, + ) + if helpers.code_check(r): + self.connection_status = r.get("connectionStatus") + self.device_status = r.get("deviceStatus") + if self.dimmable_feature: + self._brightness = int(r.get("brightNess")) + else: + logger.debug("Error getting %s details", self.device_name) + + def get_config(self) -> None: + """Get configuration of dimmable bulb.""" + body = helpers.req_body(self.manager, "devicedetail") + body["method"] = "configurations" + body["uuid"] = self.uuid + + r, _ = helpers.call_api( + "/SmartBulb/v1/device/configurations", + "post", + headers=helpers.req_headers(self.manager), + json=body, + ) + + if helpers.code_check(r): + self.config = helpers.build_config_dict(r) + else: + logger.warning("Error getting %s config info", self.device_name) + + def toggle(self, status) -> bool: + """Toggle dimmable bulb.""" + body = self._get_body(status) + r, _ = helpers.call_api( + "/SmartBulb/v1/device/devicestatus", + "put", + headers=helpers.req_headers(self.manager), + json=body, + ) + if helpers.code_check(r): + self.device_status = status + return True + return False + + def set_brightness(self, brightness: int) -> bool: + """Set brightness of dimmable bulb.""" + if not self.dimmable_feature: + logger.debug("%s is not dimmable", self.device_name) + return False + if isinstance(brightness, int) and (brightness <= 0 or brightness > 100): + + logger.warning("Invalid brightness") + return False + + body = self._get_body("on") + body["brightNess"] = str(brightness) + r, _ = helpers.call_api( + "/SmartBulb/v1/device/updateBrightness", + "put", + headers=helpers.req_headers(self.manager), + json=body, + ) + + if helpers.code_check(r): + self._brightness = brightness + return True + + logger.debug("Error setting brightness for %s", self.device_name) + return False + + def _get_body(self, status: str): + body = helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + body["status"] = status + return body + + +class VeSyncBulbESL100CW(VeSyncBulb): + """VeSync Tunable and Dimmable White Bulb.""" + + def __init__(self, details, manager): + """Initialize Etekcity Tunable white bulb.""" + super().__init__(details, manager) + + def get_details(self) -> None: + """Get details of tunable bulb.""" + body = helpers.req_body(self.manager, "bypass") + body["cid"] = self.cid + body["jsonCmd"] = {"getLightStatus": "get"} + body["configModule"] = self.config_module + r, _ = helpers.call_api( + "/cloud/v1/deviceManaged/bypass", + "post", + headers=helpers.req_headers(self.manager), + json=body, + ) + if not isinstance(r, dict) or not helpers.code_check(r): + logger.debug("Error calling %s", self.device_name) + return + response = r + if response.get("result", {}).get("light") is not None: + light = response.get("result", {}).get("light") + self.connection_status = "online" + self.device_status = light.get("action", "off") + if self.dimmable_feature: + self._brightness = light.get("brightness") + if self.color_temp_feature: + self._color_temp = light.get("colorTempe") + elif response.get("code") == -11300027: + logger.debug("%s device offline", self.device_name) + self.connection_status = "offline" + self.device_status = "off" + else: + logger.debug( + "%s - Unknown return code - %d with message %s", + self.device_name, + response.get("code"), + response.get("msg"), + ) + + def get_config(self) -> None: + """Get configuration and firmware info of tunable bulb.""" + body = helpers.req_body(self.manager, "bypass_config") + body["uuid"] = self.uuid + + r, _ = helpers.call_api( + "/cloud/v1/deviceManaged/configurations", + "post", + headers=helpers.req_headers(self.manager), + json=body, + ) + + if helpers.code_check(r): + self.config = helpers.build_config_dict(r) + else: + logger.debug("Error getting %s config info", self.device_name) + + def toggle(self, status) -> bool: + """Toggle tunable bulb.""" + body = helpers.req_body(self.manager, "bypass") + body["cid"] = self.cid + body["configModule"] = self.config_module + body["jsonCmd"] = {"light": {"action": status}} + r, _ = helpers.call_api( + "/cloud/v1/deviceManaged/bypass", + "post", + headers=helpers.req_headers(self.manager), + json=body, + ) + if helpers.code_check(r) == 0: + self.device_status = status + return True + logger.debug("%s offline", self.device_name) + self.device_status = "off" + self.connection_status = "offline" + return False + + def set_brightness(self, brightness: int) -> bool: + """Set brightness of tunable bulb.""" + if not self.dimmable_feature: + logger.debug("%s is not dimmable", self.device_name) + return False + if brightness <= 0 or brightness > 100: + logger.debug("Invalid brightness") + return False + + body = helpers.req_body(self.manager, "bypass") + body["cid"] = self.cid + body["configModule"] = self.config_module + if self.device_status == "off": + light_dict = {"action": "on", "brightness": brightness} + else: + light_dict = {"brightness": brightness} + body["jsonCmd"] = {"light": light_dict} + r, _ = helpers.call_api( + "/cloud/v1/deviceManaged/bypass", + "post", + headers=helpers.req_headers(self.manager), + json=body, + ) + + if helpers.code_check(r): + self._brightness = brightness + return True + self.device_status = "off" + self.connection_status = "offline" + logger.debug("%s offline", self.device_name) + + return False + + def set_color_temp(self, color_temp: int) -> bool: + """Set Color Temperature of Bulb in pct (1 - 100).""" + if color_temp < 0 or color_temp > 100: + logger.debug("Invalid color temperature - only between 0 and 100") + return False + body = helpers.req_body(self.manager, "bypass") + body["cid"] = self.cid + body["jsonCmd"] = {"light": {}} + if self.device_status == "off": + light_dict = {"action": "on", "colorTempe": color_temp} + else: + light_dict = {"colorTempe": color_temp} + body["jsonCmd"]["light"] = light_dict + r, _ = helpers.call_api( + "/cloud/v1/deviceManaged/bypass", + "post", + headers=helpers.req_headers(self.manager), + json=body, + ) + + if not helpers.code_check(r): + return False + + if r.get("code") == -11300027: + logger.debug("%s device offline", self.device_name) + self.connection_status = "offline" + self.device_status = "off" + return False + if r.get("code") == 0: + self.device_status = "on" + self._color_temp = color_temp + return True + logger.debug( + "%s - Unknown return code - %d with message %s", + self.device_name, + r.get("code"), + r.get("msg"), + ) + return False diff --git a/custom_components/vesync/pyvesync/vesyncfan.py b/custom_components/vesync/pyvesync/vesyncfan.py new file mode 100644 index 0000000..2de6ad1 --- /dev/null +++ b/custom_components/vesync/pyvesync/vesyncfan.py @@ -0,0 +1,1325 @@ +"""VeSync API for controlling fans and purifiers.""" + +import json +import logging +from typing import Dict, Tuple, Union + +from .helpers import Helpers +from .vesyncbasedevice import VeSyncBaseDevice + +humid_features: dict = { + "Classic300S": { + "module": "VeSyncHumid200300S", + "models": ["Classic300S", "LUH-A601S-WUSB"], + "features": ["night_light"], + "mist_modes": ["auto", "sleep", "manual"], + "mist_levels": list(range(1, 10)), + }, + "Classic200S": { + "module": "VeSyncHumid200S", + "models": ["Classic200S"], + "features": ["nightlight"], + "mist_modes": ["auto", "manual"], + "mist_levels": list(range(1, 10)), + }, + "Dual200S": { + "module": "VeSyncHumid200300S", + "models": ["Dual200S", "LUH-D301S-WUSR", "LUH-D301S-WJP", "LUH-D301S-WEU"], + "features": [], + "mist_modes": ["auto", "sleep", "manual"], + "mist_levels": list(range(1, 3)), + }, + "LV600S": { + "module": "VeSyncHumid200300S", + "models": [ + "LUH-A602S-WUSR", + "LUH-A602S-WUS", + "LUH-A602S-WEUR", + "LUH-A602S-WEU", + "LUH-A602S-WJP", + ], + "features": ["warm_mist", "night_light"], + "mist_modes": ["humidity", "sleep", "manual"], + "mist_levels": list(range(1, 10)), + "warm_mist_levels": [0, 1, 2, 3], + }, +} + + +air_features: dict = { + "Core200S": { + "module": "VeSyncAirBypass", + "models": ["Core200S", "LAP-C201S-AUSR", "LAP-C202S-WUSR"], + "modes": ["sleep", "off"], + "features": [], + "levels": list(range(1, 4)), + }, + "Core300S": { + "module": "VeSyncAirBypass", + "models": ["Core300S", "LAP-C301S-WJP"], + "modes": ["sleep", "off", "auto"], + "features": ["air_quality"], + "levels": list(range(1, 5)), + }, + "Core400S": { + "module": "VeSyncAirBypass", + "models": ["Core400S", "LAP-C401S-WJP", "LAP-C401S-WUSR", "LAP-C401S-WAAA"], + "modes": ["sleep", "off", "auto"], + "features": ["air_quality"], + "levels": list(range(1, 5)), + }, + "Core600S": { + "module": "VeSyncAirBypass", + "models": ["Core600S", "LAP-C601S-WUS", "LAP-C601S-WUSR", "LAP-C601S-WEU"], + "modes": ["sleep", "off", "auto"], + "features": ["air_quality"], + "levels": list(range(1, 5)), + }, + "LV-PUR131S": { + "module": "VeSyncAir131", + "models": ["LV-PUR131S", "LV-RH131S"], + "features": ["air_quality"], + }, +} + + +logger = logging.getLogger(__name__) + + +def model_dict() -> dict: + """Build purifier and humidifier model dictionary.""" + model_modules = {} + for dev_dict in {**air_features, **humid_features}.values(): + for model in dev_dict["models"]: + model_modules[model] = dev_dict["module"] + return model_modules + + +def model_features(dev_type: str) -> dict: + """Get features from device type.""" + for dev_dict in {**air_features, **humid_features}.values(): + if dev_type in dev_dict["models"]: + return dev_dict + raise ValueError("Device not configured") + + +fan_classes: set = {v["module"] for k, v in {**air_features, **humid_features}.items()} + +fan_modules: dict = model_dict() + +__all__: list = list(fan_classes) + ["fan_modules"] + + +class VeSyncAirBypass(VeSyncBaseDevice): + """Base class for Levoit Purifier Bypass API Calls.""" + + def __init__(self, details: Dict[str, list], manager): + """Initialize air devices.""" + super().__init__(details, manager) + self.enabled = True + self.config_dict = model_features(self.device_type) + self.features = self.config_dict.get("features", []) + if not isinstance(self.config_dict.get("modes"), list): + logger.error( + "Please set modes for %s in the configuration", self.device_type + ) + raise Exception + self.modes = self.config_dict["modes"] + if "air_quality" in self.features: + self.air_quality_feature = True + else: + self.air_quality_feature = False + self.details: Dict[str, Union[str, int, float, bool]] = { + "filter_life": 0, + "mode": "manual", + "level": 0, + "display": False, + "child_lock": False, + "night_light": "off", + } + if self.air_quality_feature is True: + self.details["ait_quality"] = 0 + self.config: Dict[str, Union[str, int, float, bool]] = { + "display": False, + "display_forever": False, + } + + def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: + """Build device api body dictionary. + + standard modes are: ['getPurifierStatus', 'setSwitch', + 'setNightLight', + 'setLevel', 'setPurifierMode', 'setDisplay', + 'setChildLock'] + """ + modes = [ + "getPurifierStatus", + "setSwitch", + "setNightLight", + "setLevel", + "setPurifierMode", + "setDisplay", + "setChildLock", + "setIndicatorLight", + ] + if method not in modes: + logger.debug("Invalid mode - %s", method) + return {}, {} + head = Helpers.bypass_header() + body = Helpers.bypass_body_v2(self.manager) + body["cid"] = self.cid + body["configModule"] = self.config_module + body["payload"] = {"method": method, "source": "APP"} + return head, body + + def build_purifier_dict(self, dev_dict: dict) -> None: + """Build Bypass purifier status dictionary.""" + self.enabled = dev_dict.get("enabled", False) + if self.enabled: + self.device_status = "on" + else: + self.device_status = "off" + self.details["filter_life"] = dev_dict.get("filter_life", 0) + self.mode = dev_dict.get("mode", "manual") + self.speed = dev_dict.get("level", 0) + self.details["display"] = dev_dict.get("display", False) + self.details["child_lock"] = dev_dict.get("child_lock", False) + self.details["night_light"] = dev_dict.get("night_light", "off") + self.details["display"] = dev_dict.get("display", False) + self.details["display_forever"] = dev_dict.get("display_forever", False) + if self.air_quality_feature: + self.details["air_quality"] = dev_dict.get("air_quality", 0) + + def build_config_dict(self, conf_dict: Dict[str, str]) -> None: + """Build configuration dict for Bypass purifier.""" + self.config["display"] = conf_dict.get("display", False) + self.config["display_forever"] = conf_dict.get("display_forever", False) + + def get_details(self) -> None: + """Build Bypass Purifier details dictionary.""" + head = Helpers.bypass_header() + body = Helpers.bypass_body_v2(self.manager) + body["cid"] = self.cid + body["configModule"] = self.config_module + body["payload"] = {"method": "getPurifierStatus", "source": "APP", "data": {}} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + if not isinstance(r, dict): + logger.debug("Error in purifier response") + return + if not isinstance(r.get("result"), dict): + logger.debug("Error in purifier response") + return + outer_result = r.get("result", {}) + inner_result = None + + if outer_result: + inner_result = r.get("result", {}).get("result") + if inner_result is not None and Helpers.code_check(r): + if outer_result.get("code") == 0: + self.build_purifier_dict(inner_result) + else: + logger.debug("error in inner result dict from purifier") + if inner_result.get("configuration", {}): + self.build_config_dict(inner_result.get("configuration", {})) + else: + logger.debug("No configuration found in purifier status") + else: + logger.debug("Error in purifier response") + + def update(self): + """Update Purifier details.""" + self.get_details() + + def change_fan_speed(self, speed=None) -> bool: + """Change fan speed based on levels in configuration dict.""" + speeds: list = self.config_dict.get("levels", []) + current_speed = self.speed + + if speed is not None: + if speed not in speeds: + logger.debug( + "%s is invalid speed - valid speeds are %s", speed, str(speeds) + ) + return False + new_speed = speed + if current_speed == new_speed: + return True + else: + if current_speed == speeds[-1]: + new_speed = speeds[0] + else: + current_index = speeds.index(current_speed) + new_speed = speeds[current_index + 1] + + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + + head, body = self.build_api_dict("setLevel") + if not head and not body: + return False + + body["payload"]["data"] = { + "id": 0, + "level": new_speed, + "type": "wind", + "mode": "manual", + } + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + self.speed = new_speed + return True + logger.debug("Error changing %s speed", self.device_name) + return False + + def child_lock_on(self) -> bool: + """Turn Bypass child lock on.""" + return self.set_child_lock(True) + + def child_lock_off(self) -> bool: + """Turn Bypass child lock off.""" + return self.set_child_lock(False) + + def set_child_lock(self, mode: bool) -> bool: + """Set Bypass child lock.""" + if mode not in (True, False): + logger.debug("Invalid mode passed to set_child_lock - %s", mode) + return False + + head, body = self.build_api_dict("setChildLock") + if not head and not body: + return False + + body["payload"]["data"] = {"child_lock": mode} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + self.details["child_lock"] = mode + return True + if isinstance(r, dict): + logger.debug("Error toggling child lock") + else: + logger.debug("Error in api return json for %s", self.device_name) + return False + + def mode_toggle(self, mode: str) -> bool: + """Set purifier mode - sleep or manual.""" + if mode.lower() not in self.modes: + logger.debug("Invalid purifier mode used - %s", mode) + return False + head, body = self.build_api_dict("setPurifierMode") + if not head and not body: + return False + + body["payload"]["data"] = {"mode": mode.lower()} + if mode == "manual": + body["payload"] = { + "data": {"id": 0, "level": 1, "type": "wind"}, + "method": "setLevel", + "type": "APP", + } + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if Helpers.code_check(r): + return True + logger.debug("Error setting purifier mode") + return False + + def manual_mode(self) -> bool: + """Set mode to manual.""" + if "manual" not in self.modes: + logger.debug("%s does not have manual mode", self.device_name) + return False + return self.mode_toggle("manual") + + def sleep_mode(self) -> bool: + """Set sleep mode to on.""" + if "sleep" not in self.modes: + logger.debug("%s does not have sleep mode", self.device_name) + return False + return self.mode_toggle("sleep") + + def auto_mode(self) -> bool: + """Set mode to auto.""" + if "auto" not in self.modes: + logger.debug("%s does not have auto mode", self.device_name) + return False + return self.mode_toggle("auto") + + def toggle_switch(self, toggle: bool) -> bool: + """Toggle purifier on/off.""" + if not isinstance(toggle, bool): + logger.debug("Invalid toggle value for purifier switch") + return False + + head = Helpers.bypass_header() + body = Helpers.bypass_body_v2(self.manager) + body["cid"] = self.cid + body["configModule"] = self.config_module + body["payload"] = { + "data": {"enabled": toggle, "id": 0}, + "method": "setSwitch", + "source": "APP", + } + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + if toggle: + self.device_status = "on" + else: + self.device_status = "off" + return True + logger.debug("Error toggling purifier - %s", self.device_name) + return False + + def turn_on(self) -> bool: + """Turn bypass Purifier on.""" + return self.toggle_switch(True) + + def turn_off(self): + """Turn Bypass Purifier off.""" + return self.toggle_switch(False) + + def set_display(self, mode: bool) -> bool: + """Toggle display on/off.""" + if not isinstance(mode, bool): + logger.debug("Mode must be True or False") + return False + + head, body = self.build_api_dict("setDisplay") + + body["payload"]["data"] = {"state": mode} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + return True + logger.debug("Error toggling purifier display - %s", self.device_name) + return False + + def turn_on_display(self) -> bool: + """Turn Display on.""" + return self.set_display(True) + + def turn_off_display(self): + """Turn Display off.""" + return self.set_display(False) + + def set_night_light(self, mode: str) -> bool: + """Set night list - on, off or dim.""" + if mode.lower() not in ["on", "off", "dim"]: + logger.debug("Invalid nightlight mode used (on, off or dim)- %s", mode) + return False + head, body = self.build_api_dict("setNightLight") + if not head and not body: + return False + body["payload"]["data"] = {"night_light": mode.lower()} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + self.details["night_light"] = mode.lower() + return True + logger.debug("Error setting nightlight mode") + return False + + @property + def air_quality(self): + """Get air quality value (ug/m3).""" + if self.air_quality_feature is not True: + logger.debug("%s does not have air quality sensor", self.device_type) + try: + return int(self.details["air_quality"]) + except KeyError: + return 0 + + @property + def fan_level(self): + """Get current fan level (1-3).""" + try: + speed = int(self.speed) + except ValueError: + speed = self.speed + return speed + + @property + def filter_life(self) -> int: + """Get percentage of filter life remaining.""" + try: + return int(self.details["filter_life"]) + except KeyError: + return 0 + + @property + def display_state(self) -> bool: + """Get display state.""" + return bool(self.details["display"]) + + @property + def screen_status(self) -> bool: + """Get display status.""" + return bool(self.details["display"]) + + @property + def child_lock(self) -> bool: + """Get child lock state.""" + return bool(self.details["child_lock"]) + + @property + def night_light(self) -> str: + """Get night light state (on/dim/off).""" + return str(self.details["night_light"]) + + def display(self) -> None: + """Return formatted device info to stdout.""" + super().display() + disp1 = [ + ("Mode: ", self.mode, ""), + ("Filter Life: ", self.details["filter_life"], "percent"), + ("Fan Level: ", self.speed, ""), + ("Display: ", self.details["display"], ""), + ("Child Lock: ", self.details["child_lock"], ""), + ("Night Light: ", self.details["night_light"], ""), + ("Display Config: ", self.config["display"], ""), + ("Display_Forever Config: ", self.config["display_forever"], ""), + ] + if self.air_quality_feature: + disp1.append(("Air Quality: ", self.details["air_quality"], "ug/m3")) + for line in disp1: + print(f"{line[0]:.<20} {line[1]} {line[2]}") + + def displayJSON(self) -> str: + """Return air purifier status and properties in JSON output.""" + sup = super().displayJSON() + sup_val = json.loads(sup) + sup_val.update( + { + "Mode": self.mode, + "Filter Life": str(self.details["filter_life"]), + "Fan Level": str(self.speed), + "Display": self.details["display"], + "Child Lock": self.details["child_lock"], + "Night Light": str(self.details["night_light"]), + "Display Config": self.config["display"], + "Display_Forever Config": self.config["display_forever"], + } + ) + if self.air_quality_feature: + sup_val.update({"Air Quality": str(self.details["air_quality"])}) + return json.dumps(sup_val) + + +class VeSyncAir131(VeSyncBaseDevice): + """Levoit Air Purifier Class.""" + + def __init__(self, details, manager): + """Initialize air purifier class.""" + super().__init__(details, manager) + + self.details: Dict = {} + + def get_details(self) -> None: + """Build Air Purifier details dictionary.""" + body = Helpers.req_body(self.manager, "devicedetail") + body["uuid"] = self.uuid + head = Helpers.req_headers(self.manager) + + r, _ = Helpers.call_api( + "/131airPurifier/v1/device/deviceDetail", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + self.device_status = r.get("deviceStatus", "unknown") + self.connection_status = r.get("connectionStatus", "unknown") + self.details["active_time"] = r.get("activeTime", 0) + self.details["filter_life"] = r.get("filterLife", {}) + self.details["screen_status"] = r.get("screenStatus", "unknown") + self.mode = r.get("mode", self.mode) + self.details["level"] = r.get("level", 0) + self.details["air_quality"] = r.get("airQuality", "unknown") + else: + logger.debug("Error getting %s details", self.device_name) + + def get_config(self) -> None: + """Get configuration info for air purifier.""" + body = Helpers.req_body(self.manager, "devicedetail") + body["method"] = "configurations" + body["uuid"] = self.uuid + + r, _ = Helpers.call_api( + "/131airpurifier/v1/device/configurations", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if r is not None and Helpers.code_check(r): + self.config = Helpers.build_config_dict(r) + else: + logger.debug("Unable to get config info for %s", self.device_name) + + @property + def active_time(self) -> int: + """Return total time active in minutes.""" + return self.details.get("active_time", 0) + + @property + def fan_level(self) -> int: + """Get current fan level (1-3).""" + return self.details.get("level", 0) + + @property + def filter_life(self) -> int: + """Get percentage of filter life remaining.""" + try: + return self.details["filter_life"].get("percent", 0) + except KeyError: + return 0 + + @property + def air_quality(self) -> str: + """Get Air Quality.""" + return self.details.get("air_quality", "unknown") + + @property + def screen_status(self) -> str: + """Return Screen status (on/off).""" + return self.details.get("screen_status", "unknown") + + def turn_on(self) -> bool: + """Turn Air Purifier on.""" + if self.device_status != "on": + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + body["status"] = "on" + head = Helpers.req_headers(self.manager) + + r, _ = Helpers.call_api( + "/131airPurifier/v1/device/deviceStatus", "put", json=body, headers=head + ) + + if r is not None and Helpers.code_check(r): + self.device_status = "on" + return True + logger.debug("Error turning %s on", self.device_name) + return False + return False + + def turn_off(self) -> bool: + """Turn Air Purifier Off.""" + if self.device_status == "on": + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + body["status"] = "off" + head = Helpers.req_headers(self.manager) + + r, _ = Helpers.call_api( + "/131airPurifier/v1/device/deviceStatus", "put", json=body, headers=head + ) + + if r is not None and Helpers.code_check(r): + self.device_status = "off" + return True + logger.debug("Error turning %s off", self.device_name) + return False + return True + + def auto_mode(self) -> bool: + """Set mode to auto.""" + return self.mode_toggle("auto") + + def manual_mode(self) -> bool: + """Set mode to manual.""" + return self.mode_toggle("manual") + + def sleep_mode(self) -> bool: + """Set sleep mode to on.""" + return self.mode_toggle("sleep") + + def change_fan_speed(self, speed: int = None) -> bool: + """Adjust Fan Speed for air purifier. + + Specifying 1,2,3 as argument or call without argument to cycle + through speeds increasing by one. + """ + if self.mode != "manual": + logger.debug("%s not in manual mode, cannot change speed", self.device_name) + return False + + try: + level = self.details["level"] + except KeyError: + logger.debug( + "Cannot change fan speed, no level set for %s", self.device_name + ) + return False + + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + head = Helpers.req_headers(self.manager) + if speed is not None: + if speed == level: + return True + if speed in [1, 2, 3]: + body["level"] = speed + else: + logger.debug("Invalid fan speed for %s", self.device_name) + return False + else: + if (level + 1) > 3: + body["level"] = 1 + else: + body["level"] = int(level + 1) + + r, _ = Helpers.call_api( + "/131airPurifier/v1/device/updateSpeed", "put", json=body, headers=head + ) + + if r is not None and Helpers.code_check(r): + self.details["level"] = body["level"] + return True + logger.debug("Error changing %s speed", self.device_name) + return False + + def mode_toggle(self, mode: str) -> bool: + """Set mode to manual, auto or sleep.""" + head = Helpers.req_headers(self.manager) + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + if mode != self.mode and mode in ["sleep", "auto", "manual"]: + body["mode"] = mode + if mode == "manual": + body["level"] = 1 + + r, _ = Helpers.call_api( + "/131airPurifier/v1/device/updateMode", "put", json=body, headers=head + ) + + if r is not None and Helpers.code_check(r): + self.mode = mode + return True + + logger.debug("Error setting %s mode - %s", self.device_name, mode) + return False + + def update(self) -> None: + """Run function to get device details.""" + self.get_details() + + def display(self) -> None: + """Return formatted device info to stdout.""" + super().display() + disp1 = [ + ("Active Time : ", self.active_time, " minutes"), + ("Fan Level: ", self.fan_level, ""), + ("Air Quality: ", self.air_quality, ""), + ("Mode: ", self.mode, ""), + ("Screen Status: ", self.screen_status, ""), + ("Filter Life: ", self.filter_life, " percent"), + ] + for line in disp1: + print(f"{line[0]:.<15} {line[1]} {line[2]}") + + def displayJSON(self) -> str: + """Return air purifier status and properties in JSON output.""" + sup = super().displayJSON() + sup_val = json.loads(sup) + sup_val.update( + { + "Active Time": str(self.active_time), + "Fan Level": self.fan_level, + "Air Quality": self.air_quality, + "Mode": self.mode, + "Screen Status": self.screen_status, + "Filter Life": str(self.filter_life), + } + ) + return sup_val + + +class VeSyncHumid200300S(VeSyncBaseDevice): + """200S/300S Humidifier Class.""" + + def __init__(self, details, manager): + """Initialize 200S/300S Humidifier class.""" + super().__init__(details, manager) + self.enabled = True + self.config_dict = model_features(self.device_type) + self.mist_levels = self.config_dict.get("mist_levels") + self.mist_modes = self.config_dict.get("mist_modes") + self.features = self.config_dict.get("features") + if "warm_mist" in self.features: + self.warm_mist_levels = self.config_dict.get("warm_mist_levels", []) + self.warm_mist_feature = True + else: + self.warm_mist_feature = False + self.warm_mist_levels = [] + if "nightlight" in self.config_dict.get("features", []): + self.night_light = True + else: + self.night_light = False + self.details: Dict[str, Union[str, int, float]] = { + "humidity": 0, + "mist_virtual_level": 0, + "mist_level": 0, + "mode": "manual", + "water_lacks": False, + "humidity_high": False, + "water_tank_lifted": False, + "display": False, + "automatic_stop_reach_target": False, + } + if self.night_light is True: + self.details["night_light_brightness"] = 0 + self.config: Dict[str, Union[str, int, float]] = { + "auto_target_humidity": 0, + "display": False, + "automatic_stop": True, + } + self._api_modes = [ + "getHumidifierStatus", + "setAutomaticStop", + "setSwitch", + "setNightLightBrightness", + "setVirtualLevel", + "setTargetHumidity", + "setHumidityMode", + "setDisplay", + "setLevel", + ] + + def build_api_dict(self, method: str) -> Tuple[Dict, Dict]: + """Build humidifier api call header and body. + + Available methods are: 'getHumidifierStatus', 'setAutomaticStop', + 'setSwitch', 'setNightLightBrightness', 'setVirtualLevel', + 'setTargetHumidity', 'setHumidityMode' + """ + if method not in self._api_modes: + logger.debug("Invalid mode - %s", method) + raise ValueError + head = Helpers.bypass_header() + body = Helpers.bypass_body_v2(self.manager) + body["cid"] = self.cid + body["configModule"] = self.config_module + body["payload"] = {"method": method, "source": "APP"} + return head, body + + def build_humid_dict(self, dev_dict: Dict[str, str]) -> None: + """Build humidifier status dictionary.""" + self.enabled = dev_dict.get("enabled") + self.details["humidity"] = dev_dict.get("humidity", 0) + self.details["mist_virtual_level"] = dev_dict.get("mist_virtual_level", 0) + self.details["mist_level"] = dev_dict.get("mist_level", 0) + self.details["mode"] = dev_dict.get("mode", "manual") + self.details["water_lacks"] = dev_dict.get("water_lacks", False) + self.details["humidity_high"] = dev_dict.get("humidity_high", False) + self.details["water_tank_lifted"] = dev_dict.get("water_tank_lifted", False) + self.details["automatic_stop_reach_target"] = dev_dict.get( + "automatic_stop_reach_target", True + ) + if self.night_light: + self.details["night_light_brightness"] = dev_dict.get( + "night_light_brightness", 0 + ) + if self.warm_mist_feature: + self.details["warm_mist_level"] = dev_dict.get("warm_level", 0) + self.details["warm_mist_enabled"] = dev_dict.get("warm_enabled", False) + try: + self.details["display"] = dev_dict["display"] + except KeyError: + self.details["display"] = dev_dict.get("indicator_light_switch", False) + + def build_config_dict(self, conf_dict): + """Build configuration dict for 300s humidifier.""" + self.config["auto_target_humidity"] = conf_dict.get("auto_target_humidity", 0) + self.config["display"] = conf_dict.get("display", False) + self.config["automatic_stop"] = conf_dict.get("automatic_stop", True) + + def get_details(self) -> None: + """Build 200S/300S Humidifier details dictionary.""" + head = Helpers.bypass_header() + body = Helpers.bypass_body_v2(self.manager) + body["cid"] = self.cid + body["configModule"] = self.config_module + body["payload"] = {"method": "getHumidifierStatus", "source": "APP", "data": {}} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + if r is None or not isinstance(r, dict): + logger.debug("Error getting status of %s ", self.device_name) + outer_result = r.get("result", {}) + inner_result = None + + if outer_result is not None: + inner_result = r.get("result", {}).get("result") + if inner_result is not None and Helpers.code_check(r): + if outer_result.get("code") == 0: + self.build_humid_dict(inner_result) + else: + logger.debug("error in inner result dict from humidifier") + if inner_result.get("configuration", {}): + self.build_config_dict(inner_result.get("configuration", {})) + else: + logger.debug("No configuration found in humidifier status") + else: + logger.debug("Error in humidifier response") + + def update(self): + """Update 200S/300S Humidifier details.""" + self.get_details() + + def toggle_switch(self, toggle: bool) -> bool: + """Toggle humidifier on/off.""" + if not isinstance(toggle, bool): + logger.debug("Invalid toggle value for humidifier switch") + return False + + head = Helpers.bypass_header() + body = Helpers.bypass_body_v2(self.manager) + body["cid"] = self.cid + body["configModule"] = self.config_module + body["payload"] = { + "data": {"enabled": toggle, "id": 0}, + "method": "setSwitch", + "source": "APP", + } + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + if toggle: + self.device_status = "on" + else: + self.device_status = "off" + + return True + logger.debug("Error toggling 300S humidifier - %s", self.device_name) + return False + + def turn_on(self) -> bool: + """Turn 200S/300S Humidifier on.""" + return self.toggle_switch(True) + + def turn_off(self): + """Turn 200S/300S Humidifier off.""" + return self.toggle_switch(False) + + def automatic_stop_on(self) -> bool: + """Turn 200S/300S Humidifier automatic stop on.""" + return self.set_automatic_stop(True) + + def automatic_stop_off(self) -> bool: + """Turn 200S/300S Humidifier automatic stop on.""" + return self.set_automatic_stop(False) + + def set_automatic_stop(self, mode: bool) -> bool: + """Set 200S/300S Humidifier to automatic stop.""" + if mode not in (True, False): + logger.debug("Invalid mode passed to set_automatic_stop - %s", mode) + return False + + head, body = self.build_api_dict("setAutomaticStop") + if not head and not body: + return False + + body["payload"]["data"] = {"enabled": mode} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + return True + if isinstance(r, dict): + logger.debug("Error toggling automatic stop") + else: + logger.debug("Error in api return json for %s", self.device_name) + return False + + def set_display(self, mode: bool) -> bool: + """Toggle display on/off.""" + if not isinstance(mode, bool): + logger.debug("Mode must be True or False") + return False + + head, body = self.build_api_dict("setDisplay") + + body["payload"]["data"] = {"state": mode} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + return True + logger.debug("Error toggling 300S display - %s", self.device_name) + return False + + def turn_on_display(self) -> bool: + """Turn 200S/300S Humidifier on.""" + return self.set_display(True) + + def turn_off_display(self): + """Turn 200S/300S Humidifier off.""" + return self.set_display(False) + + def set_humidity(self, humidity: int) -> bool: + """Set target 200S/300S Humidifier humidity.""" + if humidity < 30 or humidity > 80: + logger.debug("Humidity value must be set between 30 and 80") + return False + head, body = self.build_api_dict("setTargetHumidity") + + if not head and not body: + return False + + body["payload"]["data"] = {"target_humidity": humidity} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + return True + logger.debug("Error setting humidity") + return False + + def set_night_light_brightness(self, brightness: int) -> bool: + """Set target 200S/300S Humidifier night light brightness.""" + if not self.night_light: + logger.debug( + "%s is a %s does not have a nightlight", + self.device_name, + self.device_type, + ) + return False + if brightness < 0 or brightness > 100: + logger.debug("Brightness value must be set between 0 and 100") + return False + head, body = self.build_api_dict("setNightLightBrightness") + + if not head and not body: + return False + + body["payload"]["data"] = {"night_light_brightness": brightness} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + return True + logger.debug("Error setting humidity") + return False + + def set_humidity_mode(self, mode: str) -> bool: + """Set humidifier mode - sleep or auto.""" + if mode.lower() not in self.mist_modes: + logger.debug("Invalid humidity mode used - %s", mode) + logger.debug("Proper modes for this device are - %s", str(self.mist_modes)) + return False + head, body = self.build_api_dict("setHumidityMode") + if not head and not body: + return False + body["payload"]["data"] = {"mode": mode.lower()} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + return True + logger.debug("Error setting humidity mode") + return False + + def set_warm_level(self, warm_level) -> bool: + """Set target 600S Humidifier mist warmth.""" + if not self.warm_mist_feature: + logger.debug( + "%s is a %s does not have a mist warmer", + self.device_name, + self.device_type, + ) + return False + if not isinstance(warm_level, int): + try: + warm_level = int(warm_level) + except ValueError: + logger.debug("Error converting warm mist level to a integer") + if warm_level not in self.warm_mist_levels: + logger.debug("warm_level value must be - %s", str(self.warm_mist_levels)) + return False + head, body = self.build_api_dict("setLevel") + + if not head and not body: + return False + + body["payload"]["data"] = { + "type": "warm", + "level": warm_level, + "id": 0, + } + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + return True + logger.debug("Error setting warm") + return False + + def set_auto_mode(self): + """Set auto mode for humidifiers.""" + if "auto" in self.mist_modes: + call_str = "auto" + elif "humidity" in self.mist_modes: + call_str = "humidity" + else: + logger.debug( + "Trying auto mode, mode not set for this model, " + "please ensure %s model " + "is in configuration dictionary", + self.device_type, + ) + call_str = "auto" + set_auto = self.set_humidity_mode(call_str) + return set_auto + + def set_manual_mode(self): + """Set humifier to manual mode with 1 mist level.""" + return self.set_humidity_mode("manual") + + def set_mist_level(self, level) -> bool: + """Set humidifier mist level with int between 0 - 9.""" + try: + level = int(level) + except ValueError: + level = str(level) + if level not in self.mist_levels: + logger.debug("Humidifier mist level must be between 0 and 9") + return False + + head, body = self.build_api_dict("setVirtualLevel") + if not head and not body: + return False + + body["payload"]["data"] = {"id": 0, "level": level, "type": "mist"} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + return True + logger.debug("Error setting mist level") + return False + + @property + def humidity(self): + """Get Humidity level.""" + return self.details["humidity"] + + @property + def mist_level(self): + """Get current mist level.""" + return self.details["virtual_mist_level"] + + @property + def water_lacks(self): + """If tank is empty return true.""" + return self.details["water_lacks"] + + @property + def auto_humidity(self): + """Auto target humidity.""" + return self.config["auto_target_humidity"] + + @property + def auto_enabled(self): + """Auto mode is enabled.""" + if self.details.get("mode") == "auto" or self.details.get("mode") == "humidity": + return True + return False + + @property + def warm_mist_enabled(self): + """Warm mist feature enabled.""" + if self.warm_mist_feature: + return self.details["warm_mist_enabled"] + return False + + def display(self) -> None: + """Return formatted device info to stdout.""" + super().display() + disp1 = [ + ("Mode: ", self.details["mode"], ""), + ("Humidity: ", self.details["humidity"], "percent"), + ("Mist Virtual Level: ", self.details["mist_virtual_level"], ""), + ("Mist Level: ", self.details["mist_level"], ""), + ("Water Lacks: ", self.details["water_lacks"], ""), + ("Humidity High: ", self.details["humidity_high"], ""), + ("Water Tank Lifted: ", self.details["water_tank_lifted"], ""), + ("Display: ", self.details["display"], ""), + ( + "Automatic Stop Reach Target: ", + self.details["automatic_stop_reach_target"], + "", + ), + ("Auto Target Humidity: ", self.config["auto_target_humidity"], "percent"), + ("Automatic Stop: ", self.config["automatic_stop"], ""), + ] + if self.night_light: + disp1.append( + ( + "Night Light Brightness: ", + self.details["night_light_brightness"], + "percent", + ) + ) + if self.warm_mist_feature: + disp1.append(("Warm mist enabled: ", self.details["warm_mist_enabled"], "")) + disp1.append(("Warm mist level: ", self.details["warm_mist_level"], "")) + for line in disp1: + print(f"{line[0]:.<29} {line[1]} {line[2]}") + + def displayJSON(self) -> str: + """Return air purifier status and properties in JSON output.""" + sup = super().displayJSON() + sup_val = json.loads(sup) + sup_val.update( + { + "Mode": self.details["mode"], + "Humidity": str(self.details["humidity"]), + "Mist Virtual Level": str(self.details["mist_virtual_level"]), + "Mist Level": str(self.details["mist_level"]), + "Water Lacks": self.details["water_lacks"], + "Humidity High": self.details["humidity_high"], + "Water Tank Lifted": self.details["water_tank_lifted"], + "Display": self.details["display"], + "Automatic Stop Reach Target": self.details[ + "automatic_stop_reach_target" + ], + "Auto Target Humidity": str(self.config["auto_target_humidity"]), + "Automatic Stop": self.config["automatic_stop"], + } + ) + if self.night_light: + sup_val["Night Light Brightness"] = self.details["night_light_brightness"] + if self.warm_mist_feature: + sup_val["Warm mist enabled"] = self.details["warm_mist_enabled"] + sup_val["Warm mist level"] = self.details["warm_mist_level"] + return json.dumps(sup_val) + + +class VeSyncHumid200S(VeSyncHumid200300S): + """Levoit Classic 200S Specific class.""" + + def __init__(self, details, manager): + """Initialize levoit 200S device class.""" + super().__init__(details, manager) + self._api_modes = [ + "getHumidifierStatus", + "setAutomaticStop", + "setSwitch", + "setVirtualLevel", + "setTargetHumidity", + "setHumidityMode", + "setIndicatorLightSwitch", + ] + + def set_display(self, mode: bool) -> bool: + """Toggle display on/off.""" + if not isinstance(mode, bool): + logger.debug("Mode must be True or False") + return False + + head, body = self.build_api_dict("setIndicatorLightSwitch") + + body["payload"]["data"] = {"enabled": mode, "id": 0} + + r, _ = Helpers.call_api( + "/cloud/v2/deviceManaged/bypassV2", + method="post", + headers=head, + json=body, + ) + + if r is not None and Helpers.code_check(r): + return True + logger.debug("Error toggling 300S display - %s", self.device_name) + return False diff --git a/custom_components/vesync/pyvesync/vesyncoutlet.py b/custom_components/vesync/pyvesync/vesyncoutlet.py new file mode 100644 index 0000000..99f6645 --- /dev/null +++ b/custom_components/vesync/pyvesync/vesyncoutlet.py @@ -0,0 +1,703 @@ +"""Etekcity Outlets.""" + +from abc import ABCMeta, abstractmethod +import json +import logging +import time + +from .helpers import Helpers +from .vesyncbasedevice import VeSyncBaseDevice + +logger = logging.getLogger(__name__) + +outlet_config = { + "wifi-switch-1.3": {"module": "VeSyncOutlet7A"}, + "ESW03-USA": {"module": "VeSyncOutlet10A"}, + "ESW01-EU": {"module": "VeSyncOutlet10A"}, + "ESW15-USA": {"module": "VeSyncOutlet15A"}, + "ESO15-TB": {"module": "VeSyncOutdoorPlug"}, +} + +outlet_modules = {k: v["module"] for k, v in outlet_config.items()} + +__all__ = list(outlet_modules.values()) + ["outlet_modules"] + + +class VeSyncOutlet(VeSyncBaseDevice): + """Base class for Etekcity Outlets.""" + + __metaclass__ = ABCMeta + + def __init__(self, details, manager): + """Initialize VeSync Outlet base class.""" + super().__init__(details, manager) + + self.details = {} + self.energy = {} + self.update_energy_ts = None + self._energy_update_interval = manager.energy_update_interval + + @property + def update_time_check(self) -> bool: + """Test if energy update interval has been exceeded.""" + if self.update_energy_ts is None: + return True + + if (time.time() - self.update_energy_ts) > self._energy_update_interval: + return True + return False + + @abstractmethod + def turn_on(self) -> bool: + """Return True if device has beeeen turned on.""" + + @abstractmethod + def turn_off(self) -> bool: + """Return True if device has beeeen turned off.""" + + @abstractmethod + def get_details(self) -> None: + """Build details dictionary.""" + + @abstractmethod + def get_weekly_energy(self) -> None: + """Build weekly energy history dictionary.""" + + @abstractmethod + def get_monthly_energy(self) -> None: + """Build Monthly Energy History Dictionary.""" + + @abstractmethod + def get_yearly_energy(self): + """Build Yearly Energy Dictionary.""" + + @abstractmethod + def get_config(self): + """Get configuration and firmware details.""" + + def update(self): + """Get Device Energy and Status.""" + self.get_details() + + def update_energy(self, bypass_check: bool = False): + """Build weekly, monthly and yearly dictionaries.""" + if bypass_check or self.update_time_check: + self.update_energy_ts = time.time() + self.get_weekly_energy() + if "week" in self.energy: + self.get_monthly_energy() + self.get_yearly_energy() + if not bypass_check: + self.update_energy_ts = time.time() + + @property + def active_time(self) -> int: + """Return active time of a device in minutes.""" + return self.details.get("active_time", 0) + + @property + def energy_today(self) -> float: + """Return energy.""" + return self.details.get("energy", 0) + + @property + def power(self) -> float: + """Return current power in watts.""" + return float(self.details.get("power", 0)) + + @property + def voltage(self) -> float: + """Return current voltage.""" + return float(self.details.get("voltage", 0)) + + @property + def monthly_energy_total(self) -> float: + """Return total energy usage over the month.""" + return self.energy.get("month", {}).get("total_energy", 0) + + @property + def weekly_energy_total(self) -> float: + """Return total energy usage over the week.""" + return self.energy.get("week", {}).get("total_energy", 0) + + @property + def yearly_energy_total(self) -> float: + """Return total energy usage over the year.""" + return self.energy.get("year", {}).get("total_energy", 0) + + def display(self): + """Return formatted device info to stdout.""" + super().display() + disp1 = [ + ("Active Time : ", self.active_time, " minutes"), + ("Energy: ", self.energy_today, " kWh"), + ("Power: ", self.power, " Watts"), + ("Voltage: ", self.voltage, " Volts"), + ("Energy Week: ", self.weekly_energy_total, " kWh"), + ("Energy Month: ", self.monthly_energy_total, " kWh"), + ("Energy Year: ", self.yearly_energy_total, " kWh"), + ] + for line in disp1: + print(f"{line[0]:.<15} {line[1]} {line[2]}") + + def displayJSON(self): + """Return JSON details for outlet.""" + sup = super().displayJSON() + sup_val = json.loads(sup) + sup_val.update( + { + "Active Time": str(self.active_time), + "Energy": str(self.energy_today), + "Power": str(self.power), + "Voltage": str(self.voltage), + "Energy Week": str(self.weekly_energy_total), + "Energy Month": str(self.monthly_energy_total), + "Energy Year": str(self.yearly_energy_total), + } + ) + + return sup_val + + +class VeSyncOutlet7A(VeSyncOutlet): + """Etekcity 7A Round Outlet Class.""" + + def __init__(self, details, manager): + """Initialize Etekcity 7A round outlet class.""" + super().__init__(details, manager) + self.det_keys = ["deviceStatus", "activeTime", "energy", "power", "voltage"] + self.energy_keys = ["energyConsumptionOfToday", "maxEnergy", "totalEnergy"] + + def get_details(self) -> None: + """Get 7A outlet details.""" + r, _ = Helpers.call_api( + f"/v1/device/{self.cid}/detail", + "get", + headers=Helpers.req_headers(self.manager), + ) + + if r is not None and all(x in r for x in self.det_keys): + self.device_status = r.get("deviceStatus", self.device_status) + self.details["active_time"] = r.get("activeTime", 0) + self.details["energy"] = r.get("energy", 0) + power = r.get("power", "0:0") + power = round(float(Helpers.calculate_hex(power)), 2) + self.details["power"] = power + voltage = r.get("voltage", "0:0") + voltage = round(float(Helpers.calculate_hex(voltage)), 2) + self.details["voltage"] = voltage + else: + logger.debug("Unable to get %s details", self.device_name) + + def get_weekly_energy(self) -> None: + """Get 7A outlet weekly energy info and build weekly energy dict.""" + r, _ = Helpers.call_api( + f"/v1/device/{self.cid}/energy/week", + "get", + headers=Helpers.req_headers(self.manager), + ) + + if r is not None and all(x in r for x in self.energy_keys): + self.energy["week"] = Helpers.build_energy_dict(r) + else: + logger.debug("Unable to get %s weekly data", self.device_name) + + def get_monthly_energy(self) -> None: + """Get 7A outlet monthly energy info and build monthly energy dict.""" + r, _ = Helpers.call_api( + f"/v1/device/{self.cid}/energy/month", + "get", + headers=Helpers.req_headers(self.manager), + ) + + if r is not None and all(x in r for x in self.energy_keys): + self.energy["month"] = Helpers.build_energy_dict(r) + else: + logger.warning("Unable to get %s monthly data", self.device_name) + + def get_yearly_energy(self) -> None: + """Get 7A outlet yearly energy info and build yearly energy dict.""" + r, _ = Helpers.call_api( + f"/v1/device/{self.cid}/energy/year", + "get", + headers=Helpers.req_headers(self.manager), + ) + + if r is not None and all(x in r for x in self.energy_keys): + self.energy["year"] = Helpers.build_energy_dict(r) + else: + logger.debug("Unable to get %s yearly data", self.device_name) + + def turn_on(self) -> bool: + """Turn 7A outlet on - return True if successful.""" + return self._toggle("on") + + def turn_off(self) -> bool: + """Turn 7A outlet off - return True if successful.""" + return self._toggle("off") + + def _toggle(self, state): + _, status_code = Helpers.call_api( + f"/v1/wifi-switch-1.3/{self.cid}/status/{state}", + "put", + headers=Helpers.req_headers(self.manager), + ) + + if status_code is not None and status_code == 200: + self.device_status = state + return True + logger.warning("Error turning %s %s", self.device_name, state) + return False + + def get_config(self) -> None: + """Get 7A outlet configuration info.""" + r, _ = Helpers.call_api( + f"/v1/device/{self.cid}/configurations", + "get", + headers=Helpers.req_headers(self.manager), + ) + + if "currentFirmVersion" in r: + self.config = Helpers.build_config_dict(r) + else: + logger.debug("Error getting configuration info for %s", self.device_name) + + +class VeSyncOutlet10A(VeSyncOutlet): + """Etekcity 10A Round Outlets.""" + + def __init__(self, details, manager): + """Initialize 10A outlet class.""" + super().__init__(details, manager) + + def get_details(self) -> None: + """Get 10A outlet details.""" + body = Helpers.req_body(self.manager, "devicedetail") + body["uuid"] = self.uuid + + r, _ = Helpers.call_api( + "/10a/v1/device/devicedetail", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(r): + self.device_status = r.get("deviceStatus", self.device_status) + self.connection_status = r.get("connectionStatus", self.connection_status) + self.details = Helpers.build_details_dict(r) + else: + logger.debug("Unable to get %s details", self.device_name) + + def get_config(self) -> None: + """Get 10A outlet configuration info.""" + body = Helpers.req_body(self.manager, "devicedetail") + body["method"] = "configurations" + body["uuid"] = self.uuid + + r, _ = Helpers.call_api( + "/10a/v1/device/configurations", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(r): + self.config = Helpers.build_config_dict(r) + else: + logger.debug("Error getting %s config info", self.device_name) + + def get_weekly_energy(self) -> None: + """Get 10A outlet weekly energy info and populate energy dict.""" + body = Helpers.req_body(self.manager, "energy_week") + body["uuid"] = self.uuid + + response, _ = Helpers.call_api( + "/10a/v1/device/energyweek", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.energy["week"] = Helpers.build_energy_dict(response) + else: + logger.debug("Unable to get %s weekly data", self.device_name) + + def get_monthly_energy(self) -> None: + """Get 10A outlet monthly energy info and populate energy dict.""" + body = Helpers.req_body(self.manager, "energy_month") + body["uuid"] = self.uuid + + response, _ = Helpers.call_api( + "/10a/v1/device/energymonth", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.energy["month"] = Helpers.build_energy_dict(response) + else: + logger.debug("Unable to get %s monthly data", self.device_name) + + def get_yearly_energy(self) -> None: + """Get 10A outlet yearly energy info and populate energy dict.""" + body = Helpers.req_body(self.manager, "energy_year") + body["uuid"] = self.uuid + + response, _ = Helpers.call_api( + "/10a/v1/device/energyyear", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.energy["year"] = Helpers.build_energy_dict(response) + else: + logger.debug("Unable to get %s yearly data", self.device_name) + + def turn_on(self) -> bool: + """Turn 10A outlet on - return True if successful.""" + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + body["status"] = "on" + + response, _ = Helpers.call_api( + "/10a/v1/device/devicestatus", + "put", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.device_status = "on" + return True + logger.warning("Error turning %s on", self.device_name) + return False + + def turn_off(self) -> bool: + """Turn 10A outlet off - return True if successful.""" + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + body["status"] = "off" + + response, _ = Helpers.call_api( + "/10a/v1/device/devicestatus", + "put", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.device_status = "off" + return True + logger.warning("Error turning %s off", self.device_name) + return False + + +class VeSyncOutlet15A(VeSyncOutlet): + """Class for Etekcity 15A Rectangular Outlets.""" + + def __init__(self, details, manager): + """Initialize 15A rectangular outlets.""" + super().__init__(details, manager) + self.nightlight_status = "off" + self.nightlight_brightness = 0 + + def get_details(self) -> None: + """Get 15A outlet details.""" + body = Helpers.req_body(self.manager, "devicedetail") + body["uuid"] = self.uuid + + r, _ = Helpers.call_api( + "/15a/v1/device/devicedetail", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + attr_list = ( + "deviceStatus", + "activeTime", + "energy", + "power", + "voltage", + "nightLightStatus", + "nightLightAutomode", + "nightLightBrightness", + ) + + if Helpers.code_check(r) and all(k in r for k in attr_list): + + self.device_status = r.get("deviceStatus") + self.connection_status = r.get("connectionStatus") + self.nightlight_status = r.get("nightLightStatus") + self.nightlight_brightness = r.get("nightLightBrightness") + self.details = Helpers.build_details_dict(r) + else: + logger.debug("Unable to get %s details", self.device_name) + + def get_config(self) -> None: + """Get 15A outlet configuration info.""" + body = Helpers.req_body(self.manager, "devicedetail") + body["method"] = "configurations" + body["uuid"] = self.uuid + + r, _ = Helpers.call_api( + "/15a/v1/device/configurations", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(r): + self.config = Helpers.build_config_dict(r) + else: + logger.debug("Unable to get %s config info", self.device_name) + + def get_weekly_energy(self) -> None: + """Get 15A outlet weekly energy info and populate energy dict.""" + body = Helpers.req_body(self.manager, "energy_week") + body["uuid"] = self.uuid + + response, _ = Helpers.call_api( + "/15a/v1/device/energyweek", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.energy["week"] = Helpers.build_energy_dict(response) + else: + logger.debug("Unable to get %s weekly data", self.device_name) + + def get_monthly_energy(self) -> None: + """Get 15A outlet monthly energy info and populate energy dict.""" + body = Helpers.req_body(self.manager, "energy_month") + body["uuid"] = self.uuid + + response, _ = Helpers.call_api( + "/15a/v1/device/energymonth", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.energy["month"] = Helpers.build_energy_dict(response) + else: + logger.debug("Unable to get %s monthly data", self.device_name) + + def get_yearly_energy(self) -> None: + """Get 15A outlet yearly energy info and populate energy dict.""" + body = Helpers.req_body(self.manager, "energy_year") + body["uuid"] = self.uuid + + response, _ = Helpers.call_api( + "/15a/v1/device/energyyear", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.energy["year"] = Helpers.build_energy_dict(response) + else: + logger.debug("Unable to get %s yearly data", self.device_name) + + def turn_on(self) -> bool: + """Turn 15A outlet on - return True if successful.""" + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + body["status"] = "on" + + response, _ = Helpers.call_api( + "/15a/v1/device/devicestatus", + "put", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.device_status = "on" + return True + logger.warning("Error turning %s on", self.device_name) + return False + + def turn_off(self) -> bool: + """Turn 15A outlet off - return True if successful.""" + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + body["status"] = "off" + + response, _ = Helpers.call_api( + "/15a/v1/device/devicestatus", + "put", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.device_status = "off" + return True + + logger.warning("Error turning %s off", self.device_name) + return False + + def turn_on_nightlight(self) -> bool: + """Turn on nightlight.""" + return self._extracted_from_turn_off_nightlight_3("auto") + + def turn_off_nightlight(self) -> bool: + """Turn Off Nightlight.""" + return self._extracted_from_turn_off_nightlight_3("manual") + + def _extracted_from_turn_off_nightlight_3(self, mode): + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + body["mode"] = mode + response, _ = Helpers.call_api( + "/15a/v1/device/nightlightstatus", + "put", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + return True + logger.debug( + "Error turning %s %s nightlight", + "on" if mode == "auto" else "off", + self.device_name, + ) + return False + + +class VeSyncOutdoorPlug(VeSyncOutlet): + """Class to hold Etekcity outdoor outlets.""" + + def __init__(self, details, manager): + """Initialize Etekcity Outdoor Plug class.""" + super().__init__(details, manager) + + def get_details(self) -> None: + """Get details for outdoor outlet.""" + body = Helpers.req_body(self.manager, "devicedetail") + body["uuid"] = self.uuid + r, _ = Helpers.call_api( + "/outdoorsocket15a/v1/device/devicedetail", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(r): + self.details = Helpers.build_details_dict(r) + self.connection_status = r.get("connectionStatus") + + dev_no = self.sub_device_no + sub_device_list = r.get("subDevices") + if sub_device_list and dev_no <= len(sub_device_list): + self.device_status = sub_device_list[(dev_no + -1)].get( + "subDeviceStatus" + ) + return + logger.debug("Unable to get %s details", self.device_name) + + def get_config(self) -> None: + """Get configuration info for outdoor outlet.""" + body = Helpers.req_body(self.manager, "devicedetail") + body["method"] = "configurations" + body["uuid"] = self.uuid + + r, _ = Helpers.call_api( + "/outdoorsocket15a/v1/device/configurations", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(r): + self.config = Helpers.build_config_dict(r) + logger.debug("Error getting %s config info", self.device_name) + + def get_weekly_energy(self) -> None: + """Get outdoor outlet weekly energy info and populate energy dict.""" + body = Helpers.req_body(self.manager, "energy_week") + body["uuid"] = self.uuid + + response, _ = Helpers.call_api( + "/outdoorsocket15a/v1/device/energyweek", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.energy["week"] = Helpers.build_energy_dict(response) + else: + logger.debug("Unable to get %s weekly data", self.device_name) + + def get_monthly_energy(self) -> None: + """Get outdoor outlet monthly energy info and populate energy dict.""" + body = Helpers.req_body(self.manager, "energy_month") + body["uuid"] = self.uuid + + response, _ = Helpers.call_api( + "/outdoorsocket15a/v1/device/energymonth", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.energy["month"] = Helpers.build_energy_dict(response) + logger.debug("Unable to get %s monthly data", self.device_name) + + def get_yearly_energy(self) -> None: + """Get outdoor outlet yearly energy info and populate energy dict.""" + body = Helpers.req_body(self.manager, "energy_year") + body["uuid"] = self.uuid + + response, _ = Helpers.call_api( + "/outdoorsocket15a/v1/device/energyyear", + "post", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.energy["year"] = Helpers.build_energy_dict(response) + else: + logger.debug("Unable to get %s yearly data", self.device_name) + + def toggle(self, status) -> bool: + """Toggle power for outdoor outlet.""" + body = Helpers.req_body(self.manager, "devicestatus") + body["uuid"] = self.uuid + body["status"] = status + body["switchNo"] = self.sub_device_no + + response, _ = Helpers.call_api( + "/outdoorsocket15a/v1/device/devicestatus", + "put", + headers=Helpers.req_headers(self.manager), + json=body, + ) + + if Helpers.code_check(response): + self.device_status = status + return True + logger.warning("Error turning %s %s", self.device_name, status) + return False + + def turn_on(self) -> bool: + """Turn outdoor outlet on and return True if successful.""" + return bool(self.toggle("on")) + + def turn_off(self) -> bool: + """Turn outdoor outlet off and return True if successful.""" + return bool(self.toggle("off")) diff --git a/custom_components/vesync/pyvesync/vesyncswitch.py b/custom_components/vesync/pyvesync/vesyncswitch.py new file mode 100644 index 0000000..71c3ad9 --- /dev/null +++ b/custom_components/vesync/pyvesync/vesyncswitch.py @@ -0,0 +1,347 @@ +"""Classes for VeSync Switch Devices.""" + +from abc import ABCMeta, abstractmethod +import json +import logging +from typing import Dict, Union + +from .helpers import Helpers as helpers +from .vesyncbasedevice import VeSyncBaseDevice + +logger = logging.getLogger(__name__) + +feature_dict: Dict[str, Dict[str, Union[list, str]]] = { + "ESWL01": {"module": "VeSyncWallSwitch", "features": []}, + "ESWD16": {"module": "VeSyncDimmerSwitch", "features": ["dimmable"]}, + "ESWL03": {"module": "VeSyncWallSwitch", "features": []}, +} + +switch_modules: dict = {k: v["module"] for k, v in feature_dict.items()} + +__all__: list = list(switch_modules.values()) + ["switch_modules"] + + +class VeSyncSwitch(VeSyncBaseDevice): + """Etekcity Switch Base Class.""" + + __metaclasss__ = ABCMeta + + def __init__(self, details, manager): + """Initialize Switch Base Class.""" + super().__init__(details, manager) + self.features = feature_dict.get(self.device_type, {}).get("features") + if self.features is None: + logger.error("No configuration set for - %s", self.device_name) + raise RuntimeError(f"No configuration set for - {self.device_name}") + self.details = {} + + def is_dimmable(self) -> bool: + """Return True if switch is dimmable.""" + return bool("dimmable" in self.features) + + @abstractmethod + def get_details(self) -> None: + """Get Device Details.""" + + @abstractmethod + def turn_on(self) -> bool: + """Turn Switch On.""" + + @abstractmethod + def turn_off(self) -> bool: + """Turn switch off.""" + + @abstractmethod + def get_config(self) -> None: + """Get configuration and firmware deatils.""" + + @property + def active_time(self) -> int: + """Get active time of switch.""" + return self.details.get("active_time", 0) + + def update(self) -> None: + """Update device details.""" + self.get_details() + + +class VeSyncWallSwitch(VeSyncSwitch): + """Etekcity standard wall switch class.""" + + def __init__(self, details, manager): + """Initialize standard etekcity wall switch class.""" + super().__init__(details, manager) + + def get_details(self) -> None: + """Get switch device details.""" + body = helpers.req_body(self.manager, "devicedetail") + body["uuid"] = self.uuid + head = helpers.req_headers(self.manager) + + r, _ = helpers.call_api( + "/inwallswitch/v1/device/devicedetail", "post", headers=head, json=body + ) + + if r is not None and helpers.code_check(r): + self.device_status = r.get("deviceStatus", self.device_status) + self.details["active_time"] = r.get("activeTime", 0) + self.connection_status = r.get("connectionStatus", self.connection_status) + else: + logger.debug("Error getting %s details", self.device_name) + + def get_config(self) -> None: + """Get switch device configuration info.""" + body = helpers.req_body(self.manager, "devicedetail") + body["method"] = "configurations" + body["uuid"] = self.uuid + + r, _ = helpers.call_api( + "/inwallswitch/v1/device/configurations", + "post", + headers=helpers.req_headers(self.manager), + json=body, + ) + + if helpers.code_check(r): + self.config = helpers.build_config_dict(r) + else: + logger.warning("Unable to get %s config info", self.device_name) + + def turn_off(self) -> bool: + """Turn off switch device.""" + body = helpers.req_body(self.manager, "devicestatus") + body["status"] = "off" + body["uuid"] = self.uuid + head = helpers.req_headers(self.manager) + + r, _ = helpers.call_api( + "/inwallswitch/v1/device/devicestatus", "put", headers=head, json=body + ) + + if r is not None and helpers.code_check(r): + self.device_status = "off" + return True + logger.warning("Error turning %s off", self.device_name) + return False + + def turn_on(self) -> bool: + """Turn on switch device.""" + body = helpers.req_body(self.manager, "devicestatus") + body["status"] = "on" + body["uuid"] = self.uuid + head = helpers.req_headers(self.manager) + + r, _ = helpers.call_api( + "/inwallswitch/v1/device/devicestatus", "put", headers=head, json=body + ) + + if r is not None and helpers.code_check(r): + self.device_status = "on" + return True + logger.warning("Error turning %s on", self.device_name) + return False + + +class VeSyncDimmerSwitch(VeSyncSwitch): + """Vesync Dimmer Switch Class with RGB Faceplate.""" + + def __init__(self, details, manager): + """Initialize dimmer switch class.""" + super().__init__(details, manager) + self._brightness = 0 + self._rgb_value = {"red": 0, "blue": 0, "green": 0} + self._rgb_status = "unknown" + self._indicator_light = "unknown" + + def get_details(self) -> None: + """Get dimmer switch details.""" + body = helpers.req_body(self.manager, "devicedetail") + body["uuid"] = self.uuid + head = helpers.req_headers(self.manager) + + r, _ = helpers.call_api( + "/dimmer/v1/device/devicedetail", "post", headers=head, json=body + ) + + if r is not None and helpers.code_check(r): + self.device_status = r.get("deviceStatus", self.device_status) + self.details["active_time"] = r.get("activeTime", 0) + self.connection_status = r.get("connectionStatus", self.connection_status) + self._brightness = r.get("brightness") + self._rgb_status = r.get("rgbStatus") + self._rgb_value = r.get("rgbValue") + self._indicator_light = r.get("indicatorlightStatus") + else: + logger.debug("Error getting %s details", self.device_name) + + @property + def brightness(self) -> float: + """Return brightness in percent.""" + return self._brightness + + @property + def indicator_light_status(self) -> str: + """Faceplate brightness light status.""" + return self._indicator_light + + @property + def rgb_light_status(self) -> str: + """RGB Faceplate light status.""" + return self._rgb_status + + @property + def rgb_light_value(self) -> dict: + """RGB Light Values.""" + return self._rgb_value + + def switch_toggle(self, status: str) -> bool: + """Toggle switch status.""" + if status not in ["on", "off"]: + logger.debug("Invalid status passed to wall switch") + return False + body = helpers.req_body(self.manager, "devicestatus") + body["status"] = status + body["uuid"] = self.uuid + head = helpers.req_headers(self.manager) + + r, _ = helpers.call_api( + "/dimmer/v1/device/devicestatus", "put", headers=head, json=body + ) + + if r is not None and helpers.code_check(r): + self.device_status = status + return True + + logger.warning("Error turning %s %s", self.device_name, status) + return False + + def turn_on(self) -> bool: + """Turn switch on.""" + return self.switch_toggle("on") + + def turn_off(self) -> bool: + """Turn switch off.""" + return self.switch_toggle("off") + + def indicator_light_toggle(self, status: str) -> bool: + """Toggle indicator light.""" + if status not in ["on", "off"]: + logger.debug("Invalid status for wall switch") + return False + body = helpers.req_body(self.manager, "devicestatus") + body["status"] = status + body["uuid"] = self.uuid + head = helpers.req_headers(self.manager) + + r, _ = helpers.call_api( + "/dimmer/v1/device/indicatorlightstatus", "put", headers=head, json=body + ) + + if r is not None and helpers.code_check(r): + self.device_status = status + return True + + logger.warning("Error turning %s indicator light %s", self.device_name, status) + return False + + def indicator_light_on(self) -> bool: + """Turn Indicator light on.""" + return self.indicator_light_toggle("on") + + def indicator_light_off(self) -> bool: + """Turn indicator light off.""" + return self.indicator_light_toggle("off") + + def rgb_color_status( + self, status: str, red: int = None, blue: int = None, green: int = None + ) -> bool: + """Set faceplate RGB color.""" + body = helpers.req_body(self.manager, "devicestatus") + body["status"] = status + body["uuid"] = self.uuid + head = helpers.req_headers(self.manager) + if red is not None and blue is not None and green is not None: + body["rgbValue"] = {"red": red, "blue": blue, "green": green} + + r, _ = helpers.call_api( + "/dimmer/v1/device/devicergbstatus", "put", headers=head, json=body + ) + + if r is not None and helpers.code_check(r): + self._rgb_status = status + if body.get("rgbValue") is not None: + self._rgb_value = {"red": red, "blue": blue, "green": green} + return True + logger.warning("Error turning %s off", self.device_name) + return False + + def rgb_color_off(self) -> bool: + """Turn RGB Color Off.""" + return self.rgb_color_status("off") + + def rgb_color_on(self) -> bool: + """Turn RGB Color Off.""" + return self.rgb_color_status("on") + + def rgb_color_set(self, red: int, green: int, blue: int) -> bool: + """Set RGB color of faceplate.""" + if isinstance(red, int) and isinstance(green, int) and isinstance(blue, int): + for color in [red, green, blue]: + if color < 0 or color > 255: + logger.warning("Invalid RGB value") + return False + + return bool(self.rgb_color_status("on", red, green, blue)) + return False + + def set_brightness(self, brightness: int) -> bool: + """Set brightness of dimmer - 1 - 100.""" + if isinstance(brightness, int) and (brightness > 0 or brightness <= 100): + + body = helpers.req_body(self.manager, "devicestatus") + body["brightness"] = brightness + body["uuid"] = self.uuid + head = helpers.req_headers(self.manager) + + r, _ = helpers.call_api( + "/dimmer/v1/device/updatebrightness", "put", headers=head, json=body + ) + + if r is not None and helpers.code_check(r): + self._brightness = brightness + return True + logger.warning("Error setting %s brightness", self.device_name) + else: + logger.warning("Invalid brightness") + return False + + def displayJSON(self) -> str: + """JSON API for dimmer switch.""" + sup_val = json.loads(super().displayJSON()) + if self.is_dimmable: + sup_val.update( + { + "Indicator Light": str(self.active_time), + "Brightness": str(self._brightness), + "RGB Light": str(self._rgb_status), + } + ) + return sup_val + + def get_config(self) -> None: + """Get dimmable switch device configuration info.""" + body = helpers.req_body(self.manager, "devicedetail") + body["method"] = "configurations" + body["uuid"] = self.uuid + + r, _ = helpers.call_api( + "/dimmer/v1/device/configurations", + "post", + headers=helpers.req_headers(self.manager), + json=body, + ) + + if helpers.code_check(r): + self.config = helpers.build_config_dict(r) + else: + logger.warning("Unable to get %s config info", self.device_name) diff --git a/custom_components/vesync/sensor.py b/custom_components/vesync/sensor.py new file mode 100644 index 0000000..5c2336c --- /dev/null +++ b/custom_components/vesync/sensor.py @@ -0,0 +1,202 @@ +"""Support for power & energy sensors for VeSync outlets.""" +import logging + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .common import VeSyncBaseEntity, is_humidifier +from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_SENSORS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches.""" + + @callback + def discover(devices): + """Add new devices to platform.""" + _setup_entities(devices, async_add_entities) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_SENSORS), discover) + ) + + _setup_entities( + hass.data[DOMAIN][config_entry.entry_id][VS_SENSORS], async_add_entities + ) + + +@callback +def _setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + entities = [] + for dev in devices: + if DEV_TYPE_TO_HA.get(dev.device_type) == "outlet": + entities.extend((VeSyncPowerSensor(dev), VeSyncEnergySensor(dev))) + elif is_humidifier(dev.device_type): + entities.append(VeSyncHumiditySensor(dev)) + else: + _LOGGER.warning( + "%s - Unknown device type - %s", dev.device_name, dev.device_type + ) + + async_add_entities(entities, update_before_add=True) + + +class VeSyncOutletSensorEntity(VeSyncBaseEntity, SensorEntity): + """Representation of a sensor describing diagnostics of a VeSync outlet.""" + + def __init__(self, plug): + """Initialize the VeSync outlet device.""" + super().__init__(plug) + self.smartplug = plug + + @property + def entity_category(self): + """Return the diagnostic entity category.""" + return EntityCategory.DIAGNOSTIC + + +class VeSyncPowerSensor(VeSyncOutletSensorEntity): + """Representation of current power use for a VeSync outlet.""" + + @property + def unique_id(self): + """Return unique ID for power sensor on device.""" + return f"{super().unique_id}-power" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} current power" + + @property + def device_class(self): + """Return the power device class.""" + return SensorDeviceClass.POWER + + @property + def native_value(self): + """Return the current power usage in W.""" + return self.smartplug.power + + @property + def native_unit_of_measurement(self): + """Return the Watt unit of measurement.""" + return POWER_WATT + + @property + def state_class(self): + """Return the measurement state class.""" + return SensorStateClass.MEASUREMENT + + def update(self): + """Update outlet details and energy usage.""" + self.smartplug.update() + self.smartplug.update_energy() + + +class VeSyncEnergySensor(VeSyncOutletSensorEntity): + """Representation of current day's energy use for a VeSync outlet.""" + + def __init__(self, plug): + """Initialize the VeSync outlet device.""" + super().__init__(plug) + self.smartplug = plug + + @property + def unique_id(self): + """Return unique ID for power sensor on device.""" + return f"{super().unique_id}-energy" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} energy use today" + + @property + def device_class(self): + """Return the energy device class.""" + return SensorDeviceClass.ENERGY + + @property + def native_value(self): + """Return the today total energy usage in kWh.""" + return self.smartplug.energy_today + + @property + def native_unit_of_measurement(self): + """Return the kWh unit of measurement.""" + return ENERGY_KILO_WATT_HOUR + + @property + def state_class(self): + """Return the total_increasing state class.""" + return SensorStateClass.TOTAL_INCREASING + + def update(self): + """Update outlet details and energy usage.""" + self.smartplug.update() + self.smartplug.update_energy() + + +class VeSyncHumidifierSensorEntity(VeSyncBaseEntity, SensorEntity): + """Representation of a sensor describing diagnostics of a VeSync humidifier.""" + + def __init__(self, humidifier): + """Initialize the VeSync humidifier device.""" + super().__init__(humidifier) + self.smarthumidifier = humidifier + + @property + def entity_category(self): + """Return the diagnostic entity category.""" + return EntityCategory.DIAGNOSTIC + + +class VeSyncHumiditySensor(VeSyncHumidifierSensorEntity): + """Representation of current humidity for a VeSync humidifier.""" + + @property + def unique_id(self): + """Return unique ID for humidity sensor on device.""" + return f"{super().unique_id}-humidity" + + @property + def name(self): + """Return sensor name.""" + return f"{super().name} current humidity" + + @property + def device_class(self): + """Return the humidity device class.""" + return SensorDeviceClass.HUMIDITY + + @property + def native_value(self): + """Return the current humidity in percent.""" + return self.smarthumidifier.details["humidity"] + + @property + def native_unit_of_measurement(self): + """Return the % unit of measurement.""" + return PERCENTAGE + + @property + def state_class(self): + """Return the measurement state class.""" + return SensorStateClass.MEASUREMENT diff --git a/custom_components/vesync/services.yaml b/custom_components/vesync/services.yaml new file mode 100644 index 0000000..da264ea --- /dev/null +++ b/custom_components/vesync/services.yaml @@ -0,0 +1,3 @@ +update_devices: + name: Update devices + description: Add new VeSync devices to Home Assistant diff --git a/custom_components/vesync/strings.json b/custom_components/vesync/strings.json new file mode 100644 index 0000000..8359691 --- /dev/null +++ b/custom_components/vesync/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "user": { + "title": "Enter Username and Password", + "data": { + "username": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/custom_components/vesync/switch.py b/custom_components/vesync/switch.py new file mode 100644 index 0000000..7980cb1 --- /dev/null +++ b/custom_components/vesync/switch.py @@ -0,0 +1,203 @@ +"""Support for VeSync switches.""" +import logging + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .common import VeSyncBaseEntity, VeSyncDevice, is_humidifier +from .const import DEV_TYPE_TO_HA, DOMAIN, VS_DISCOVERY, VS_SWITCHES + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up switches.""" + + @callback + def discover(devices): + """Add new devices to platform.""" + _setup_entities(devices, async_add_entities) + + config_entry.async_on_unload( + async_dispatcher_connect(hass, VS_DISCOVERY.format(VS_SWITCHES), discover) + ) + + _setup_entities( + hass.data[DOMAIN][config_entry.entry_id][VS_SWITCHES], async_add_entities + ) + + +@callback +def _setup_entities(devices, async_add_entities): + """Check if device is online and add entity.""" + entities = [] + for dev in devices: + if DEV_TYPE_TO_HA.get(dev.device_type) == "outlet": + entities.append(VeSyncSwitchHA(dev)) + elif DEV_TYPE_TO_HA.get(dev.device_type) == "switch": + entities.append(VeSyncLightSwitch(dev)) + elif is_humidifier(dev.device_type): + entities.extend( + ( + VeSyncHumidifierDisplayHA(dev), + VeSyncHumidifierAutomaticStopHA(dev), + VeSyncHumidifierAutoOnHA(dev), + ) + ) + + else: + _LOGGER.warning( + "%s - Unknown device type - %s", dev.device_name, dev.device_type + ) + continue + + async_add_entities(entities, update_before_add=True) + + +class VeSyncBaseSwitch(VeSyncDevice, SwitchEntity): + """Base class for VeSync switch Device Representations.""" + + def turn_on(self, **kwargs): + """Turn the device on.""" + self.device.turn_on() + + +class VeSyncSwitchHA(VeSyncBaseSwitch, SwitchEntity): + """Representation of a VeSync switch.""" + + def __init__(self, plug): + """Initialize the VeSync switch device.""" + super().__init__(plug) + self.smartplug = plug + + @property + def extra_state_attributes(self): + """Return the state attributes of the device.""" + return ( + { + "voltage": self.smartplug.voltage, + "weekly_energy_total": self.smartplug.weekly_energy_total, + "monthly_energy_total": self.smartplug.monthly_energy_total, + "yearly_energy_total": self.smartplug.yearly_energy_total, + } + if hasattr(self.smartplug, "weekly_energy_total") + else {} + ) + + def update(self): + """Update outlet details and energy usage.""" + self.smartplug.update() + self.smartplug.update_energy() + + +class VeSyncLightSwitch(VeSyncBaseSwitch, SwitchEntity): + """Handle representation of VeSync Light Switch.""" + + def __init__(self, switch): + """Initialize Light Switch device class.""" + super().__init__(switch) + self.switch = switch + + +class VeSyncHumidifierSwitchEntity(VeSyncBaseEntity, SwitchEntity): + """Representation of a switch for configuring a VeSync humidifier.""" + + def __init__(self, humidifier): + """Initialize the VeSync humidifier device.""" + super().__init__(humidifier) + self.smarthumidifier = humidifier + + @property + def entity_category(self): + """Return the configuration entity category.""" + return EntityCategory.CONFIG + + +class VeSyncHumidifierDisplayHA(VeSyncHumidifierSwitchEntity): + """Representation of the display on a VeSync humidifier.""" + + @property + def unique_id(self): + """Return the ID of this display.""" + return f"{super().unique_id}-display" + + @property + def name(self): + """Return the name of the display.""" + return f"{super().name} display" + + @property + def is_on(self): + """Return True if display is on.""" + return self.device.details["display"] + + def turn_on(self, **kwargs): + """Turn the display on.""" + self.device.turn_on_display() + + def turn_off(self, **kwargs): + """Turn the display off.""" + self.device.turn_off_display() + + +class VeSyncHumidifierAutomaticStopHA(VeSyncHumidifierSwitchEntity): + """Representation of the automatic stop toggle on a VeSync humidifier.""" + + @property + def unique_id(self): + """Return the ID of this device.""" + return f"{super().unique_id}-automatic-stop" + + @property + def name(self): + """Return the name of the device.""" + return f"{super().name} automatic stop" + + @property + def is_on(self): + """Return True if automatic stop is on.""" + return self.device.config["automatic_stop"] + + def turn_on(self, **kwargs): + """Turn the automatic stop on.""" + self.device.automatic_stop_on() + + def turn_off(self, **kwargs): + """Turn the automatic stop off.""" + self.device.automatic_stop_off() + + +class VeSyncHumidifierAutoOnHA(VeSyncHumidifierSwitchEntity): + """Provide switch to turn off auto mode and set manual mist level 1 on a VeSync humidifier.""" + + @property + def unique_id(self): + """Return the ID of this device.""" + return f"{super().unique_id}-auto-mode" + + @property + def name(self): + """Return the name of the device.""" + return f"{super().name} auto mode" + + @property + def is_on(self): + """Return True if in auto mode.""" + return self.device.details["mode"] == "auto" + + def turn_on(self, **kwargs): + """Turn auto mode on.""" + self.device.set_auto_mode() + + def turn_off(self, **kwargs): + """Turn auto off by setting manual and mist level 1.""" + self.device.set_manual_mode() + self.device.set_mist_level(1) diff --git a/custom_components/vesync/translations/bg.json b/custom_components/vesync/translations/bg.json new file mode 100644 index 0000000..bb496b3 --- /dev/null +++ b/custom_components/vesync/translations/bg.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u0430", + "username": "E-mail \u0430\u0434\u0440\u0435\u0441" + }, + "title": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438 \u043f\u0430\u0440\u043e\u043b\u0430" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/ca.json b/custom_components/vesync/translations/ca.json new file mode 100644 index 0000000..0768905 --- /dev/null +++ b/custom_components/vesync/translations/ca.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introdueix el nom d'usuari i contrasenya" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/cs.json b/custom_components/vesync/translations/cs.json new file mode 100644 index 0000000..6834b79 --- /dev/null +++ b/custom_components/vesync/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ji\u017e nastaveno. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace." + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + }, + "title": "Zadejte u\u017eivatelsk\u00e9 jm\u00e9no a heslo" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/da.json b/custom_components/vesync/translations/da.json new file mode 100644 index 0000000..4803995 --- /dev/null +++ b/custom_components/vesync/translations/da.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Emailadresse" + }, + "title": "Indtast brugernavn og adgangskode" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/de.json b/custom_components/vesync/translations/de.json new file mode 100644 index 0000000..bd1ba32 --- /dev/null +++ b/custom_components/vesync/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail" + }, + "title": "Benutzername und Passwort eingeben" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/el.json b/custom_components/vesync/translations/el.json new file mode 100644 index 0000000..0fd6807 --- /dev/null +++ b/custom_components/vesync/translations/el.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u0388\u03c7\u03b5\u03b9 \u03ae\u03b4\u03b7 \u03c1\u03c5\u03b8\u03bc\u03b9\u03c3\u03c4\u03b5\u03af. \u039c\u03cc\u03bd\u03bf \u03bc\u03af\u03b1 \u03b4\u03b9\u03b1\u03bc\u03cc\u03c1\u03c6\u03c9\u03c3\u03b7 \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae." + }, + "error": { + "invalid_auth": "\u039c\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2 \u03ad\u03bb\u03b5\u03b3\u03c7\u03bf\u03c2 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2" + }, + "step": { + "user": { + "data": { + "password": "\u039a\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2", + "username": "Email" + }, + "title": "\u0395\u03b9\u03c3\u03ac\u03b3\u03b5\u03c4\u03b5 \u03cc\u03bd\u03bf\u03bc\u03b1 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03ba\u03b1\u03b9 \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/en.json b/custom_components/vesync/translations/en.json new file mode 100644 index 0000000..91e220e --- /dev/null +++ b/custom_components/vesync/translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email" + }, + "title": "Enter Username and Password" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/es-419.json b/custom_components/vesync/translations/es-419.json new file mode 100644 index 0000000..ec0fcaf --- /dev/null +++ b/custom_components/vesync/translations/es-419.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Ingrese nombre de usuario y contrase\u00f1a" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/es.json b/custom_components/vesync/translations/es.json new file mode 100644 index 0000000..dd5f95a --- /dev/null +++ b/custom_components/vesync/translations/es.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + }, + "title": "Introduzca el nombre de usuario y la contrase\u00f1a" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/et.json b/custom_components/vesync/translations/et.json new file mode 100644 index 0000000..50cd017 --- /dev/null +++ b/custom_components/vesync/translations/et.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine." + }, + "error": { + "invalid_auth": "Tuvastamise viga" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "E-post" + }, + "title": "Sisesta kasutajanimi ja salas\u00f5na" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/fr.json b/custom_components/vesync/translations/fr.json new file mode 100644 index 0000000..80bb38b --- /dev/null +++ b/custom_components/vesync/translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "invalid_auth": "Authentification invalide" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Email" + }, + "title": "Entrez vos identifiants" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/he.json b/custom_components/vesync/translations/he.json new file mode 100644 index 0000000..a5aa8e0 --- /dev/null +++ b/custom_components/vesync/translations/he.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u05ea\u05e6\u05d5\u05e8\u05ea\u05d5 \u05db\u05d1\u05e8 \u05e0\u05e7\u05d1\u05e2\u05d4. \u05e8\u05e7 \u05ea\u05e6\u05d5\u05e8\u05d4 \u05d0\u05d7\u05ea \u05d0\u05e4\u05e9\u05e8\u05d9\u05ea." + }, + "error": { + "invalid_auth": "\u05d0\u05d9\u05de\u05d5\u05ea \u05dc\u05d0 \u05d7\u05d5\u05e7\u05d9" + }, + "step": { + "user": { + "data": { + "password": "\u05e1\u05d9\u05e1\u05de\u05d4", + "username": "\u05d3\u05d5\u05d0\"\u05dc" + }, + "title": "\u05d4\u05d6\u05df \u05e9\u05dd \u05de\u05e9\u05ea\u05de\u05e9 \u05d5\u05e1\u05d9\u05e1\u05de\u05d4" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/hu.json b/custom_components/vesync/translations/hu.json new file mode 100644 index 0000000..91956af --- /dev/null +++ b/custom_components/vesync/translations/hu.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "M\u00e1r konfigur\u00e1lva van. Csak egy konfigur\u00e1ci\u00f3 lehets\u00e9ges." + }, + "error": { + "invalid_auth": "\u00c9rv\u00e9nytelen hiteles\u00edt\u00e9s" + }, + "step": { + "user": { + "data": { + "password": "Jelsz\u00f3", + "username": "E-mail" + }, + "title": "\u00cdrja be a felhaszn\u00e1l\u00f3nevet \u00e9s a jelsz\u00f3t" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/id.json b/custom_components/vesync/translations/id.json new file mode 100644 index 0000000..968f854 --- /dev/null +++ b/custom_components/vesync/translations/id.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Sudah dikonfigurasi. Hanya satu konfigurasi yang diizinkan." + }, + "error": { + "invalid_auth": "Autentikasi tidak valid" + }, + "step": { + "user": { + "data": { + "password": "Kata Sandi", + "username": "Email" + }, + "title": "Masukkan Nama Pengguna dan Kata Sandi" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/it.json b/custom_components/vesync/translations/it.json new file mode 100644 index 0000000..38f0c51 --- /dev/null +++ b/custom_components/vesync/translations/it.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione." + }, + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email" + }, + "title": "Immettere nome utente e password" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/ja.json b/custom_components/vesync/translations/ja.json new file mode 100644 index 0000000..66d7cf2 --- /dev/null +++ b/custom_components/vesync/translations/ja.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u3059\u3067\u306b\u8a2d\u5b9a\u3055\u308c\u3066\u3044\u307e\u3059\u3002\u5358\u4e00\u306e\u8a2d\u5b9a\u3057\u304b\u3067\u304d\u307e\u305b\u3093\u3002" + }, + "error": { + "invalid_auth": "\u7121\u52b9\u306a\u8a8d\u8a3c" + }, + "step": { + "user": { + "data": { + "password": "\u30d1\u30b9\u30ef\u30fc\u30c9", + "username": "E\u30e1\u30fc\u30eb" + }, + "title": "\u30e6\u30fc\u30b6\u30fc\u540d\u3068\u30d1\u30b9\u30ef\u30fc\u30c9\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/ko.json b/custom_components/vesync/translations/ko.json new file mode 100644 index 0000000..a3f1fc1 --- /dev/null +++ b/custom_components/vesync/translations/ko.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + }, + "title": "\uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/lb.json b/custom_components/vesync/translations/lb.json new file mode 100644 index 0000000..5aab1d7 --- /dev/null +++ b/custom_components/vesync/translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "error": { + "invalid_auth": "Ong\u00eblteg Authentifikatioun" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + }, + "title": "Benotzernumm a Passwuert aginn" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/lv.json b/custom_components/vesync/translations/lv.json new file mode 100644 index 0000000..eab9821 --- /dev/null +++ b/custom_components/vesync/translations/lv.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Parole", + "username": "E-pasta adrese" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/nl.json b/custom_components/vesync/translations/nl.json new file mode 100644 index 0000000..ab33023 --- /dev/null +++ b/custom_components/vesync/translations/nl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Al geconfigureerd. Slecht \u00e9\u00e9n configuratie mogelijk." + }, + "error": { + "invalid_auth": "Ongeldige authenticatie" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mail" + }, + "title": "Voer gebruikersnaam en wachtwoord in" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/no.json b/custom_components/vesync/translations/no.json new file mode 100644 index 0000000..4eaa522 --- /dev/null +++ b/custom_components/vesync/translations/no.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." + }, + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-post" + }, + "title": "Fyll inn brukernavn og passord" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/pl.json b/custom_components/vesync/translations/pl.json new file mode 100644 index 0000000..ebd17f6 --- /dev/null +++ b/custom_components/vesync/translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja." + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + }, + "title": "Wprowad\u017a nazw\u0119 u\u017cytkownika i has\u0142o." + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/pt-BR.json b/custom_components/vesync/translations/pt-BR.json new file mode 100644 index 0000000..c656860 --- /dev/null +++ b/custom_components/vesync/translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o invalida" + }, + "step": { + "user": { + "title": "Digite o nome de usu\u00e1rio e a senha" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/pt.json b/custom_components/vesync/translations/pt.json new file mode 100644 index 0000000..fb4e459 --- /dev/null +++ b/custom_components/vesync/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel." + }, + "error": { + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Endere\u00e7o de email" + }, + "title": "Introduza o nome de utilizador e a palavra-passe" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/ru.json b/custom_components/vesync/translations/ru.json new file mode 100644 index 0000000..b3ac096 --- /dev/null +++ b/custom_components/vesync/translations/ru.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "title": "VeSync" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/sk.json b/custom_components/vesync/translations/sk.json new file mode 100644 index 0000000..c043ef9 --- /dev/null +++ b/custom_components/vesync/translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_auth": "Neplatn\u00e9 overenie" + }, + "step": { + "user": { + "data": { + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/sl.json b/custom_components/vesync/translations/sl.json new file mode 100644 index 0000000..c1069e0 --- /dev/null +++ b/custom_components/vesync/translations/sl.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Vnesite uporabni\u0161ko Ime in Geslo" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/sv.json b/custom_components/vesync/translations/sv.json new file mode 100644 index 0000000..b9eedc2 --- /dev/null +++ b/custom_components/vesync/translations/sv.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "L\u00f6senord", + "username": "E-postadress" + }, + "title": "Ange anv\u00e4ndarnamn och l\u00f6senord" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/tr.json b/custom_components/vesync/translations/tr.json new file mode 100644 index 0000000..8b4f8b6 --- /dev/null +++ b/custom_components/vesync/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + }, + "title": "Kullan\u0131c\u0131 Ad\u0131 ve \u015eifre Girin" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/uk.json b/custom_components/vesync/translations/uk.json new file mode 100644 index 0000000..7f6b3a4 --- /dev/null +++ b/custom_components/vesync/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e. \u041c\u043e\u0436\u043d\u0430 \u0434\u043e\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "VeSync" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/zh-Hans.json b/custom_components/vesync/translations/zh-Hans.json new file mode 100644 index 0000000..4147846 --- /dev/null +++ b/custom_components/vesync/translations/zh-Hans.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_auth": "\u9a8c\u8bc1\u7801\u65e0\u6548" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "username": "\u7535\u5b50\u90ae\u4ef6" + }, + "title": "\u8f93\u5165\u7528\u6237\u540d\u548c\u5bc6\u7801" + } + } + } +} \ No newline at end of file diff --git a/custom_components/vesync/translations/zh-Hant.json b/custom_components/vesync/translations/zh-Hant.json new file mode 100644 index 0000000..264ad23 --- /dev/null +++ b/custom_components/vesync/translations/zh-Hant.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + }, + "title": "\u8acb\u8f38\u5165\u4f7f\u7528\u8005\u540d\u7a31\u8207\u5bc6\u78bc" + } + } + } +} \ No newline at end of file diff --git a/hacs.json b/hacs.json new file mode 100644 index 0000000..f0f6974 --- /dev/null +++ b/hacs.json @@ -0,0 +1,4 @@ +{ + "name": "VeSync 2", + "render_readme": true +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f71e1a6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[tool.black] +target-version = ["py38"] +exclude = 'generated' + +[tool.isort] +# https://github.com/PyCQA/isort/wiki/isort-Settings +profile = "black" +# will group `import x` and `from x import` of the same module. +force_sort_within_sections = true +known_first_party = [ + "homeassistant", + "tests", +] +forced_separate = [ + "tests", +] +combine_as_imports = true \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100644 index 0000000..b5b86bf --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1,5 @@ +homeassistant +black +isort +flake8 +pre-commit \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..dc2283b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,17 @@ +[flake8] +exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build +max-complexity = 25 +doctests = True +# To work with Black +# E501: line too long +# W503: Line break occurred before a binary operator +# E203: Whitespace before ':' +# D202 No blank lines allowed after function docstring +# W504 line break after binary operator +ignore = + E501, + W503, + E203, + D202, + W504 +noqa-require-code = True